1.C++介绍
C++课程包括:C++语法、STL、高阶数据结构
C++参考文档:Reference - C++ Reference
C++ 参考手册 - cppreference.com
cppreference.com
C++兼容之前学习的C语言
2.C++的第一个程序
打印hello world
#define _CRT_SECURE_NO_WARNINGS 1
// test.cpp
// 这⾥的std cout等我们都看不懂,没关系,下⾯我们会依次讲解
#include<iostream>
using namespace std;
int main()
{
cout << "hello world\n" << endl;
return 0;
}
在这个代码中我们有很多地方不清楚
-
头文件 #include
-
main函数上面的代码:using namespace std;
-
打印hello world的代码:cout << "hello world\n" << endl;
那么下面我们就逐次进行学习这些我们不懂的东西
3.命名空间namespace
1.namespace的价值
在C/C++中,变量、函数和后⾯要学到的类都是⼤量存在的,这些变量、函数和类的名称将都存在于全局作⽤域中,可能会导致很多冲突。使⽤命名空间的⽬的是对标识符的名称进⾏本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
c语⾔项⽬类似下⾯程序这样的命名冲突是普遍存在的问题,C++引⼊namespace就是为了更好的解决这样的问题
#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
int main()
{
// 编译报错:error C2365: “rand”: 重定义;以前的定义是“函数”
printf("%d\n", rand);
return 0;
}
//当头文件只有stdio.h的时候是没有问题的,但是我们加上另一个头文件stdlib.h就会出现报错的现象
//在stdlib这个库里面定义了一个rand 的函数,那么我们现在又在外面定义一个rand的变量,那么就会出现冲突
namespace就是命名空间
那么命名空间是什么样子的呢?
2.namespace的定义
• 定义命名空间,需要使⽤到namespace关键字,后⾯跟命名空间的名字,然后接⼀对{}即可,{}中
即为命名空间的成员。命名空间中可以定义变量/函数/类型等。就不会冲突了
将我们要定义的函数,结构体,全局变量都能放到这个空间中,就不会出现冲突的现象了
#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
namespace bit
{
// 命名空间中可以定义变量/函数/类型
int rand = 10;
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node* next;
int val;
};
}
int main()
{
// 编译报错:error C2365: “rand”: 重定义;以前的定义是“函数”
printf("%d\n", rand);
return 0;
}
//我们在命名空间中命名全局变量或者函数的话就不会出现冲突了
• namespace本质是定义出⼀个域,这个域跟全局域各⾃独⽴,不同的域可以定义同名变量,所以下⾯的rand不在冲突了。
我们上面的代码一开始加上stdlib会报错的原因是因为stdlib这个头文件展开会有一个叫rand的函数,那么就会于我们主函数中的变量rand起冲突,这两个rand在同一个域中,一定会有问题的
但是现在我们将rand这个全局变量加在了命名空间中,那么这个rand和stdlib的rand函数就在不同的域中,就不会互相影响了
通过域作用限定符 :: 访问域中的全局变量:
#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
namespace bit
{
// 命名空间中可以定义变量/函数/类型
int rand = 10;
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node* next;
int val;
};
}
//int main()
//{
// printf("%d\n", rand);
// return 0;
//}
//我们在命名空间中命名全局变量或者函数的话就不会出现冲突了
/*
但是我们现在打印这个rand显示的是这个rand是函数指针,
那么就说明这个rand是这个stdlib头文件展开的里面的rand函数
那么我们应该如何将我们命名空间中的rand的值打印出来呢?
如果我们想访问这个命名空间内的rand,那么我们需要指定一个这个空间的域
*/
int main()
{
printf("%p\n", rand);//00007FFB89514D50
//::域作用限定符
printf("%d\n", bit::rand);//10
//那么这里就说明我们指定要访问bit这个命名空间域里面的rand
bit::Add(1, 2);
struct bit::Node node;
//我们在域作用限定符来创建结构体的时候
//我们需要将这个操作符放到结构体类型的名称的前面
return 0;
}
/*
那么第一个打印的就是我们stdlib中展开的函数rand的地址
第二个打印的就是我们bit这个域中的rand的大小
*/
::域作用限定符 是两个冒号,通过这个符号我们能访问我们在命名空间中命名的量
我们在域作用限定符来创建结构体的时候
我们需要将这个操作符放到结构体类型的名称的前面
不同的域我们是能定义同名的,但是同一个域是不能定义同名的
• C++中域有函数局部域,全局域,命名空间域,类域;域影响的是编译时语法查找⼀个变量/函数/
类型出处(声明或定义)的逻辑,所有有了域隔离,名字冲突就解决了。局部域和全局域除了会影响
编译查找逻辑,还会影响变量的⽣命周期,命名空间域和类域不影响变量⽣命周期。
上面命名空间bit里面的rand是局部的还是全局的呢?
是全局的
因为这个命名空间并不影响这些变量的声明周期
我们只是用这个域进行了一个隔离,那就不怕和库里面冲突了
不同类型的域的访问:
int x = 0;//全局域
namespace bit
{
int x = 1;//命名空间域
}
void func()
{
int x = 2;//局部域
}
int main()
{
int x = 3;//局部域
printf("%d\n", x);//这里默认访问的是局部的这个x,因为搜索是先局部再全局搜索,不会到命名空间域和函数域中进行搜索的
printf("%d\n", bit::x);//指定一下命名空间就行了
printf("%d\n", ::x);//这么就是访问全局变量,左边是空的
return 0;
}
//局部域是会影响生命周期的,出了作用域就会被销毁了
//我们是不能访问func这个函数内的局部域的,局部域只能在当前这个域内进行访问的
//那么我们可以访问自己局部的,可以访问全局的,可以访问命名空间内的
//命名空间主要是与全局进行隔离,因为局部域一开始就是被隔离的
命名空间域:bit::x
全局域:::x
局部域:x
• namespace只能定义在全局,当然他还可以嵌套定义。
命名空间只能定义在全局中,不能定义在局部域之中
命名空间是能进行嵌套的,但是嵌套的主要是什么问题呢?怎么嵌套呢?
我们在一个大的类别的命名域中嵌套放着两个多个命名域
我们在bit这个命名空间里面能嵌套一个航哥的命名空间以及鹏哥的命名空间,各自进行隔离,各玩各的,就不存在冲突的问题了
命名空间的嵌套以及空间内的嵌套的访问
// 命名空间可以嵌套
namespace bit
{
// 鹏哥
namespace pg
{
int rand = 1;
int Add(int left, int right)
{
return left + right;
}
}
// 杭哥
namespace hg
{
int rand = 2;
int Add(int left, int right)
{
return (left + right) * 10;
}
}
}
int main()
{
printf("%d\n", bit::pg::rand);
printf("%d\n", bit::hg::rand);
printf("%d\n", bit::pg::Add(1, 2));
printf("%d\n", bit::hg::Add(1, 2));
return 0;
}
• 项⽬⼯程中多⽂件中定义的同名namespace会认为是⼀个namespace,不会冲突。
就相当于合并在一起了
• C++标准库都放在⼀个叫std(standard)的命名空间中。
域做到了名字的隔离,同一个域不能定义同一个变量,不同的域可以定义同一个变量
3.命名空间使⽤
编译查找⼀个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间⾥⾯去查找。所以下⾯程序会编译报错。所以我们要使⽤命名空间中定义的变量/函数,有三种⽅式:
• 指定命名空间访问,项⽬中推荐这种⽅式。
bit::a
• using将命名空间中某个成员展开,项⽬中经常访问的不存在冲突的成员推荐这种⽅式。
• 展开命名空间中全部成员,项⽬不推荐,冲突⻛险很⼤,⽇常⼩练习程序为了⽅便推荐使⽤。
我们在使用命名空间中的变量或者函数总是要写bit:x
就很复杂,那么我们可以在前面加上using namespace bit
正常的话我们是需要这样写才能访问命名空间内的变量或者函数
我们这个正常的搜索是局部再到全局
加上这个我们就能搜索命名空间内的变量或者函数了
namespace bit
{
int a = 0;
int b = 10;
}
//using namespace bit;//暴露这个命名空间内的所有东西
using bit::a;//将指定的变量展露出来,相当于这里将a暴露到全局出来了
int b = 2;
int main()
{
printf("%d\n", a);
printf("%d\n", a);
printf("%d\n", a);
printf("%d\n", a);
printf("%d\n", a);
printf("%d\n", a);
bit::b++;
printf("%d\n", bit::b);//11 命名空间里面的
printf("%d\n", b);//2 全局的
return 0;
}
/*
在这里我们将a暴露出来,b就正常的方法进行使用,b不暴露
*/
可以选择暴露命名空间内的所有东西,也能暴露某一个东西
4.C++输入&输出
• 是 Input Output Stream 的缩写,是标准的输⼊、输出流库,定义了标准的输⼊、输
出对象。
io流
• std::cin 是 istream 类的对象,它主要⾯向窄字符(narrow characters (of type char))的标准输
⼊流。
• std::cout 是 ostream 类的对象,它主要⾯向窄字符的标准输出流。
• std::endl 是⼀个函数,流插⼊输出时,相当于插⼊⼀个换⾏字符加刷新缓冲区。
• >是流提取运算符。(C语⾔还⽤这两个运算符做位运算左移/右移)
• 使⽤C++输⼊输出更⽅便,不需要像printf/scanf输⼊输出时那样,需要⼿动指定格式,C++的输⼊输出可以⾃动识别变量类型(本质是通过函数重载实现的,这个以后会讲到),其实最重要的C++的流能更好的⽀持⾃定义类型对象的输⼊输出。
• IO流涉及类和对象,运算符重载、继承等很多⾯向对象的知识,这些知识我们还没有讲解,所以这
⾥我们只能简单认识⼀下C++ IO流的⽤法,后⾯我们会有专⻔的⼀个章节来细节IO流库。
• cout/cin/endl等都属于C++标准库,C++标准库都放在⼀个叫std(standard)的命名空间中,所以要通过命名空间的使⽤⽅式去⽤他们。
基本的使用
using namespace std;//加上这个我们下面就可以不用指定了
//就是下面的输入和输出的话我们是不用在前面加std::的
using std::cout;//仅仅值展开一些
using std::endl;
int main()
{
// <<这个运算符叫做流插入---输出
//std::cout << "hello world\n";//可以输出任意类型的变量和对象
cout << "hello world\n";//可以输出任意类型的变量和对象
//将这个字符串流向控制台---输出
int i = 10;
cout << i<<'\n'<<"\n";
//支持连续的流插入的 换行就这么写
double d = 1.1;
cout << d << endl;
// std::cout << d << std::endl;
//右边的对象流向左边的控制台对象里面
//输入
//std::cin >> i >> d;//输入i 再接着输入d
cin >> i >> d;//输入i 再接着输入d
cout << i << ' ' << d << std::endl; //输出i 再接着输出d 再输出换行
return 0;
}
/*
我们这里是不需要像c语言一样要在前面定义%s %d 这些东西
*/
/*
换行的两种方法:在我们输出的后面接着输出换行'\n'或者是"\n"
另一种方法就是在后面输出std::endl
endl就是end line的缩写 结束一行
endl这个实际上是一个函数
std::endl这个的兼容性相较于'\n'的话更强一些
根据平台的不同会进行处理的
*/
/*
输入输出的话我们用c语言的scanf和printf都是可以的
但是我们要指定类型
c++的输入和输出就不用指定类型了
C/C++是可以混在一起用的,不会有什么问题的
如果想控制打印出来的小数点的精度的话,建议使用printf
因为C++的话控制这个精度的话会很麻烦的
*/
总结:
std::out<<i<<'\n'
输出i且输出换行
out<<i<<'\n'
输出i且输出换行 前提是前面加上using namespace 或者是using std::cout 将这个展开
cin >> i >> d;
输入i再输入d
前提是前面加上using namespace 或者是using std::cin
将这个展开
cout << i << ' ' << d << std::endl;
输出i和输出d再输出换行
换行就是std::endl
如果前面加上了using namespace 或者是using std::endl
那么我们就这么写:cout << i << ' ' << d << endl;
那么这么写就很方便了
#include<iostream>
using namespace std;
int main()
{
// 在io需求⽐较⾼的地⽅,如部分⼤量输⼊的竞赛题中,加上以下3⾏代码
// 可以提⾼C++IO效率
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
return 0;
}
对于这些输入输出的话我们不是直接操作台,我们的数据都要经历缓冲区
这样就会使效率降低
那么我们加上这个主函数里面的代码就能提高效率了
将这三串代码放在前面,后面的输出输出的话效率会高一些
在io需求比较高的地方,比如大量输入的竞赛题中,加上这三行代码
就能提高C++ IO效率
• ⼀般⽇常练习中我们可以using namespace std,实际项⽬开发中不建议using namespace std。
一般的练习的话我们是将std进行展开的,项目开发是不建议展开的
避免我们的创建的变量和库中的变量冲突了
• 这⾥我们没有包含,也可以使⽤printf和scanf,在包含间接包含了。vs系列编译器是这样的,其他编译器可能会报错。
学到这里我们来回顾一开始打印hell world的代码,这么我们就很清楚每一步的作用了
5.缺省参数
c语言是没有缺省参数的概念
C++支持缺省参数,有些地方将缺省参数叫做默认参数
• 缺省参数是声明或定义函数时为函数的参数指定⼀个缺省值。在调⽤该函数时,如果没有指定实参则采⽤该形参的缺省值,否则使⽤指定的实参,缺省参数分为全缺省和半缺省参数。(有些地⽅把缺省参数也叫默认参数)
• 全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
• 带缺省参数的函数调⽤,C++规定必须从左到右依次给实参,不能跳跃给实参。
• 函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值。
缺省参数的简单介绍:
#include <iostream>
#include <assert.h>
using namespace std;
void Func(int a = 0)
{
cout << a << endl;
}
int main()
{
Func(); // 没有传参时,使⽤参数的默认值
Func(10); // 传参时,使⽤指定的实参
return 0;
}
//如果我们没有传参的话,我们就使用的是这个缺省值
//对于这个函数func的话,我们没有传参的话,那么我们就用函数内的缺省参数作为默认值
//如果传了参的话,我们就用传的参
//那么第一个函数打印的就是0
//第二个函数打印的就是10
缺省函数分为全缺省和半缺省
全缺省就是在函数部分我们直接在括号中将所有的变量都进行定定义大小 void Func1(int a = 10, int b = 20, int c = 30)
半缺省的话就是存在没有定义大小的变量 ,就是缺省部分参数
void Func2(int a, int b = 10, int c = 20)
我们是不支持不连续的传参
比如:Func(,2,);
这种写法就是错的
只要缺省一部分那么就是半缺省
我们是不能第一个参数不缺省,第二个参数缺省,第三个参数不缺省
既然缺省了就要连续缺省
缺省函数以及半缺省函数的介绍用法
#include <iostream>
using namespace std;
// 全缺省 缺省全部参数
void Func1(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
// 半缺省 缺省部分参数
void Func2(int a, int b = 10, int c = 20)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
int main()
{
Func1();//可以一个都不传,全用的就是缺省值
Func1(1);//仅仅传第一个给a,剩下的b和c用缺省值
Func1(1, 2);//传a和b,c用缺省值
Func1(1, 2, 3);//三个都传,那么三个都不用缺省值
//Func1(,2,);//这样写是错的,因为我们这么写不支持,因为我们传参要连续传参
Func2(100);
Func2(100, 200);
Func2(100, 200, 300);
return 0;
}
/*
a = 10
b = 20
c = 30
a = 1
b = 20
c = 30
a = 1
b = 2
c = 30
a = 1
b = 2
c = 3
a = 100
b = 10
c = 20
a = 100
b = 200
c = 20
a = 100
b = 200
c = 300
*/
缺省参数的实际应用,我们在创建栈的时候我们需要手动去调节栈的大小,我们就可以设置缺省值,给这个栈设置一个初始的大小,啥都不用去传
如果我们提前知道要开辟多大的空间我们直接给,避免扩容的时候做出的消耗
如果在函数的声明中给了缺省函数的话我们就没必要再定义中再给缺省函数了
避免给的值不一样,就算一样也会报错的
如果函数没有声明的话我们是可以直接在定义中给上缺省函数的
做人不能做缺省参数,就是备胎舔狗
6.函数重载
C++⽀持在同⼀作⽤域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同。这样C++函数调⽤就表现出了多态⾏为,使⽤更灵活。C语⾔是不⽀持同⼀作⽤域中出现同名函数的。
同一个作用域中的两个同名参数,总得有点不同
形参不同,参数个数不同,类型不同
函数重载的举例
using namespace std;
//参数类型不同
int Add(int left, int right)//对整数的加法
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
double Add(double left, double right)//对小数的加法
{
cout << "double Add(double left, double right)" << endl;
return left + right;
}
// 2、参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
// 3、参数类型顺序不同
void f(int a, char b)
{
cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
cout << "f(char b, int a)" << endl;
}
//语法上下面的两个函数构成重载,但是我们在调用的时候就会有歧义了
//我们不知道调用的是哪个
//语法上是可以存在的,但是在实际运用中是不能存在的
void f1()
{
cout << "f()" << endl;
}
void f1(int a = 10)
{
cout << "f(int a)" << endl;
}
int main()
{
//下面的两个函数其实是不同的函数
//参数的类型不同
Add(10, 20);
Add(10.1, 20.2);
//参数个数不同
f();
f(10);
//参数顺序不同 本质还是类型不同
f(10, 'a');
f('a', 10);
return 0;
}
/*
如果是返回值不同那么能不能构成重载呢?
答案是不能的,因为光凭借这个我们是不能进行判断的
但是我们的参数个数以及参数顺序和参数类型不同都能进行判断函数的不同,那么就能进行重载
*/
参数不同、参数的类型不同、参数的个数不同
随便满足一个条件的话都构成函数重载
那么我们在调用的时候就不会担心调用的时候调用错了
7.引用
引⽤不是新定义⼀个变量,⽽是给已存在变量取了⼀个别名,编译器不会为引⽤变量开辟内存空间,它和它引⽤的变量共⽤同⼀块内存空间。
引用就是取别名
引用的基本用法:
using namespace std;
void Swap(int& rx, int& ry)//之前我们是没有&这个稍微,那么这里的参数就是a和b的拷贝
//因为是传值所以是不能进行根本性的交换的
//所以我们在进行交换函数的时候我们是要传地址的,使用指针的
//那么现在我们在函数的定义中我们直接将a的别名和b的别名定义在里面
//那么因为rx是a的别名,ry是b的别名,那么我们通过这两个别名就能访问他们的地址了
//并且我们在这个主函数里面我们在进行交换函数的时候我们是不需要进行取地址的操作的
//因为我们的函数传的参是a 那么我们为a取别名叫rx 那么rx就是a
//那么rx和ry的交换就是a和e的交换
//那么就是形参的改变就改变了实参
{
int tmp = rx;
rx = ry;
ry = tmp;
}
int main()
{
int a = 0;
//int b = a;//将a赋值给b
int& b = a;//给a的这块空间取别名。叫b
int& c = a;//再给a取别名叫c
int& d = b;//也可以给b取别名
//其实就是这么一块空间有多个名字
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
//这四个变量的地址都是一样的
b++;
//那么我们进行b++的话,a c d 的大小都会被改变的
//我们还能给指针p1取别名
int* p1 = &a;
int*& p2 = p1;
int e = 10;
p2 = &e;//将p2改变指向从a变成e
//因为p1就是p2
//那么随着p2的改变,那么p1也改变了
cout << *p1 << endl;
int x = 0, y = 1;
cout << x << " " << y << endl;
Swap(x, y);
cout << x << " " << y << endl;
return 0;
return 0;
}
//我们这里是给变量取别名
//但是typedef是给类型取别名
//typedef int type
//
//define是定义宏
//&这个在c语言中是取地址的符号,但是我们在这里是引用的意思
//就是给这个变量起个小名
//引用是不能替代指针的
引用等价于指针的利用:
在函数传参的时候我们使用引用我们既不用传地址了
typedef struct ListNode
{
int val;
struct ListNode* next;
}LTNode, * PNode;
// 指针变量也可以取别名,这⾥LTNode*& phead就是给指针变量取别名
// 这样就不需要⽤⼆级指针了,相对⽽⾔简化了程序
//void ListPushBack(LTNode** phead, int x)
//void ListPushBack(LTNode*& phead, int x)
void ListPushBack(PNode& phead, int x)
{
PNode newnode = (PNode)malloc(sizeof(LTNode));
newnode->val = x;
newnode->next = NULL;
if (phead == NULL)
{
phead = newnode;
}
else
{
//...
}
}
int main()
{
PNode plist = NULL;
ListPushBack(plist, 1); //那么我们传上去的plist的别名就是phead
return 0;
}
//将plist引用一个新的名字
引用就是给我们的变量换个名字
这些名字都指向了这个变量,地址都是相同的
引用就是取别名
我叫XX凯
朋友叫我凯子
这两个名字都指向的是我本人
这就是引用
一个变量是可以有多个名字的
引用的特点就是在新名字前面加上&这个符号
类型&引用别名=引用对象
在定义变量,在类型后面加&这个就是引用,其他的位置都是取地址的意思
我们给一个变量取了别名,我们还能给别名取别名,最后这些名字都指向最开始的变量
引用的特性
• 引⽤在定义时必须初始化
int&c;
这种写法就是错的,没有进行初始化
• ⼀个变量可以有多个引⽤
一个变量可以起很多个别名
• 引⽤⼀旦引⽤⼀个实体,再不能引⽤其他实体
就是我现在是你的别名,那么我就不能变成别人的别名了
引用的特性以及为什么引用不能替代指针:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
int main()
{
int a=10;
int &b=a;
int &d=b;
cout<<&a<<endl;
cout<<&b<<endl;
cout<<&d<<endl;
int e=20;
//这里并非让b引用c,因为c++引用不能改变指向
//那么这里就是赋值
//相当与对a b c d这四个变量进行赋值为e的大小
//因为前面四个变量都指向a这块空间
//现在我们重新赋值为20
//那么下面的输出就是20
d=e;
cout<<&a<<endl;
cout<<&b<<endl;
cout<<&d<<endl;
cout<<&e<<endl;
return 0;
}
/*
C++的引用是不能替代指针的
我们拿链表来举例子的话
链表的节点在物理上并非总是连续的
我们是一个节点存储着下个节点的指针的
如果我们将2这个节点删除的话
那么我们要将1的next指针的指向指向3这个节点
那么通过指针的改变就能实现
但是我们的引用是不能改变指向的
这就是为什么C++的引用不能去替代指针
*/
引用的使用
• 引⽤在实践中主要是于引⽤传参和引⽤做返回值中减少拷⻉提⾼效率和改变引⽤对象时同时改变被引⽤对象。
b是a的别名,b没有额外的开辟空间
通过应用的方式减少额外的拷贝:
void Swap(int& rx, int& ry)//形参是实参的别名
//那么rx和ry的交换就是x和y的交换
// 改变引用对象的同时能改变被引用对象
//这里的话指针也是可以实现的,但是指针的话相较于引用更加麻烦
{
int tmp = rx;
rx = ry;
ry = tmp;
}
struct A
{
int arr[1000];
};
void func(A& aa)
{
}
int main()
{
int x = 0, y = 1;
cout << x << " " << y << endl;
Swap(x, y);
cout << x << " " << y << endl;
A aa1;
func(aa1);//传指针的话我们就直接传过去指针的4个字节
//如果将这个结构体传过去的话,那么就是4000个字节,我们需要额外进行拷贝的操作
//我们在这里使用引用的话,其实更方便些,同样和指针一样不需要进行额外的拷贝的操作
return 0;
}
传值返回也会生成拷贝的
传值返回就是返回的就是拷贝的
传引用返回,返回的就是它的别名
引用做返回值可以减少拷贝做临时对象那一步,
引用减少了返回值的拷贝的步骤:
//传值返回
int STTop(ST& rs)
{
assert(rs.top > 0);
return rs.a[rs.top];
}
//STTop(st1)++ 这个是会报错的
//因为我们在传值返回的时候我们是临时创建了一块空间
//将值传过去,接收完之后这块空间就销毁了
//那么我们这里就无法进行这个返回值加加的操作了
//这个加加操作的话我们加到了临时变量上面去了
//传引用返回
int& STTop(ST& rs)
{
assert(rs.top > 0);
return rs.a[rs.top];
}
STTop(st1)++;
//我们的返回值的别名
//就是返回我们要返回值的别名
//这样我们就能进行加加操作了
//这里我们的返回是返回值的引用
//那么我们这里返回的话就直接跨过了创建临时变量的那一步了
简单点来说就是正常的传值返回的话,我们在返回的时候生成了一个中间变量,通过这个变量我们实现了值的返回
但是我们在使用引用返回的话,我们就不需要这个额外的步骤了
我们直接将返回值的引用值返回了
C++设计引用不是替代指针的,是用来简化指针的问题
不是所有的返回都能用引用返回的
//不是所有的情况都能用引用反回的
int* func()
{
int ret = 10;
ret++;
//......
return &ret;//这里我们如果返回的是ret的地址的话,是会报错的,因为ret是个局部变量
//出了函数就会被销毁的,那么返回ret的地址就是返回了一个野指针了
}
int& func1()
{
int ret = 10;
ret++;
//......
return ret;
}
//如果用的是引用返回呢?
//一样是不行的
//因为我们的ret已经别销毁了
为什么在上面的栈写的代码我们可以返回呢
因为我们的数据是在堆上面动态开辟的
我们返回的时候是一直存在的
直到这个我们主动进行free的,否则是一直存在的
那么我们就能进行返回了
那么在这个代码中就不一样,一出函数就被销毁了,指向那块空间的指针就成了野指针了
所以我们在使用引用返回的时候我们是要判断这块空间是否出函数存在
越界是不存在报错的
• 引⽤传参跟指针传参功能是类似的,引⽤传参相对更⽅便⼀些。
• 引⽤返回值的场景相对⽐较复杂,我们在这⾥简单讲了⼀下场景,还有⼀些内容后续类和对象章节中会继续深⼊讲解。
• 引⽤和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引⽤跟其他语⾔的引⽤(如Java)是有很⼤的区别的,除了⽤法,最⼤的点,C++引⽤定义后不能改变指向,Java的引⽤可以改变指向。
• ⼀些主要⽤C代码实现版本数据结构教材中,使⽤C++引⽤替代指针传参,⽬的是简化程序,避开复杂的指针,但是很多同学没学过引⽤,导致⼀头雾⽔。
关于越界访问:
int main()
{
int a[10] = {0};
//越界是不一定报错的
//越界读的话不报错 读就是打印里面的值
cout << a[10] << endl;
cout << a[11] << endl;
cout << a[12] << endl;
//越界写不一定报错 写就是进行赋值
a[10] = 1;
a[12] = 1;
return 0;
}
//编译器系统对越界行为是一种抽查行为
//不一定会报错的
8.const引用
const引用
• 可以引⽤⼀个const对象,但是必须⽤const引⽤。const引⽤也可以引⽤普通对象,因为对象的访问权限在引⽤过程中可以缩⼩,但是不能放⼤。
权限的放大和缩小:
int main()
{
const int a = 10;
//a不能进行修改,所以不能取别名,取别名的话是会对a造成修改的风险的
//在引用的过程中权限不能放大
//int& ra = a;
//下面我们引用一个const对象,但是必须用const引用
//那么这里的ra就是const对象
const int& ra = a;
//cnost引用普通的对象
//权限可以缩小
int b = 1;
const int& rb = b;
//rb不能进行修改,b能被修改
//rb++;
b++;
//不是权限的放大
//x不能修改,但是我们在下面将x拷贝成y
//我们修改y的话是对x没有影响的
const int x = 0;
int y = x;//这里是拷贝赋值
return 0;
}
/*
我们对a进行const的话,那么a的别名就不能进行别名的定义了
因为别名会影响到a,这种属于权限放大了,我们只能进行权限缩小,不能放大
但是我们创建了个变量b的话,我们是能对b进行修改的
但是如果b的别名加上const的话就不能进行修改了
如果我们一开始就将x进行const定义了
那么我们是能将x赋值给y的
这种属于拷贝
不会对x造成影响的
*/
指针上的权限问题:
int main()
{
const int a = 10;//a是不能进行修改的
const int* p1 = &a;
//p1是不能给p2的,这种属于权限的放大
//int* p2 = p1;
/*
我们的p2一开始就被固定指向为a
那么我们就不能修改指向了
因为我们前面的加了const
const在*右边就说明我们已经将这个变量进行限制了
不能将指向改变了
在左边的话就说您我们不能通过指针对指向的数据进行修改的操作
*/
int b = 20;
int* p3 = &b;//p3存的是b的地址
const int* p4=p3;//p4也是b的地址,但是我们只能进行读取,不能进行修改的
//下面的const在*右边 就说明我们是不能进行指针指向的修改的
int* const p4 = &b;
int* p5 = p4;
return 0;
}
/*
不想让const修改p就把p放在*右边-- *const p--一直指向一个数
不想让你通过p修改p的指向的内容就把const放在左边,将*p固定住-conat *p
*p被const固定住,*p指向内容的大小不得改变
*/
不想让const修改p就把p放在*右边-- *const p--一直指向一个数 不想让你通过p修改p的指向的内容就把const放在左边,将*p固定住-conat *p
p被const固定住,p指向内容的大小不得改变
• 需要注意的是类似 int& rb = a3; double d = 12.34; int& rd = d; 这样⼀些场景下a3的和结果保存在⼀个临时对象中, int& rd = d 也是类似,在类型转换中会产⽣临时对象存储中间值,也就是时,rb和rd引⽤的都是临时对象,⽽C++规定临时对象具有常性,所以这⾥就触发了权限放⼤,必须要⽤常引⽤才可以。
• 所谓临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象,C++中把这个未命名对象叫做临时对象。
const引用能引用以下对象:
1.普通对象
2.临时对象
3.const对象
不用的引用对象的使用:
void f1(const int& rx)//我们在引用接受对象的同时,为了不能通过这个形参改变实参,我们可以在2前面加上const
{
}
int main()
{
const int xx = 20;
int a = 10;
//本质而言这里是一个权限放大
//int& rb = a * 3;//引用的是这个表达式的整体 ,表达式的结果会用一个临时对象进行存储的
const int& rb = a * 3;//加上const就行了
//上面的rb引用的是个临时对象 a*3的结果存储在临时变量里面
//因为C++规定临时对象具有常性,那么这里就触发了权限放大,必须要用常引用才行
//那么前面加上const就行了
double d = 12.34;
int rd = d;
//我们是不能用int类型的数据引用其他类型的数据的
//int &rd = d;
//这里因为类型不同,那么类型转换的同时中间会生成临时变量,那么我们就需要const进行修饰
//这里的临时对象具有常性,和常量一样,就像是已经被const修饰一样
//那么我们的左边同样也要进行const修饰的操作,本质是触发了权限的放大
const int& rd = d;//前面加上const就行了
f1(xx);//可以传const对象
f1(a);//可以传普通值
f1(a * 3);//可以传表达式
f1(d);//可以传带有类型转换的
//如果用const引用传参的话是会很宽泛的
return 0;
}
//临时对象是具有常量性值,就相当与已经被const修饰了。不能被修改
//const引用能引用普通对象,引用临时对象,引用const对象
引用和指针的关系
C++中指针和引⽤就像两个性格迥异的亲兄弟,指针是哥哥,引⽤是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有⾃⼰的特点,互相不可替代。
• 语法概念上引⽤是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间。
• 引⽤在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
• 引⽤在初始化时引⽤⼀个对象后,就不能再引⽤其他对象;⽽指针可以在不断地改变指向对象。
• 引⽤可以直接访问指向对象,指针需要解引⽤才是访问指向对象。
• sizeof中含义不同,引⽤结果为引⽤类型的⼤⼩,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte)
• 指针很容易出现空指针和野指针的问题,引⽤很少出现,引⽤使⽤起来相对更安全⼀些。
9.inline
• ⽤inline修饰的函数叫做内联函数,编译时C++编译器会在调⽤的地⽅展开内联函数,这样调⽤内联函数就不需要建⽴栈帧了,就可以提⾼效率。
宏定义的缺点:
为什么会有这个inline这个东西?
// 实现⼀个ADD宏函数的常⻅问题
//#define ADD(int a, int b) return a + b;
//#define ADD(a, b) a + b;
//#define ADD(a, b) (a + b)
// 正确的宏实现
#define ADD(a, b) ((a) + (b))
// 为什么不能加分号?宏是替换机制的 ,在宏定义后面加分号的话就有问题了
// 为什么要加外⾯的括号? 不加外面的括号这个优先级就有问题了
// 为什么要加⾥⾯的括号? 同样是为了保持优先级
int main()
{
int ret = ADD(1, 2);//宏是替换机制的
cout << ret << endl;
;
cout << ADD(1, 2) << endl; //第一个问题
cout << ADD(1, 2) * 5 << endl; //第二个问题
int x = 1, y = 2;
ADD(x & y, x | y); // -> (x&y+x|y) 第三个问题
return 0;
}
/*
宏函数缺点很多,但是替换机制,调用时不用简历栈帧,提效,开销小
*/
//那么我们的祖师爷忍不了一点儿,就创建了这个inline
• inline对于编译器⽽⾔只是⼀个建议,也就是说,你加了inline编译器也可以选择在调⽤的地⽅不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适⽤于频繁调⽤的短⼩函数,对于递归函数,代码相对多⼀些的函数,加上inline也会被编译器忽略。
debug是默认不展开的
release是展开的
大家如果想要展开的话跟随下面的操作图进行操作
可以使debug能展开
1.右击项目点击属性
2.改成下图的样子就行了
3.调整完这个点击应用再确定
展开了就没有栈帧的消耗,不展开就有
如果函数较长的话,就不展开了
inline是一个对于编译器建议,编译器啊才有最终决策权
这个就是编译器的防御策略,怕不靠谱的程序员
• C语⾔实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不⽅便调试,C++设计了inline⽬的就是替代C的宏函数。
• vs编译器 debug版本下⾯默认是不展开inline的,这样⽅便调试debug版本想展开需要设置⼀下以下两个地⽅。
• inline不建议声明和定义分离到两个⽂件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。
内联函数不能分离到两个文件,因为内联函数是没有地址的
10.nullptr
NULL实际是⼀个宏,在传统的C头⽂件(stddef.h)中,可以看到如下代码
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
• C++中NULL可能被定义为字⾯常量0,或者C中被定义为⽆类型指针(void)的常量。不论采取何种定义,在使⽤空值的指针时,都不可避免的会遇到⼀些⿇烦,本想通过f(NULL)调⽤指针版本的f(int)函数,但是由于NULL被定义成0,调⽤了f(int x),因此与程序的初衷相悖。f((void*)NULL);调⽤会报错。
• C++11中引⼊nullptr,nullptr是⼀个特殊的关键字,nullptr是⼀种特殊类型的字⾯量,它可以转换成任意其他类型的指针类型。使⽤nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,⽽不能被转换为整数类型。
在c++中使用nullptr来定义空指针可以避免类型转换的问题
用nullptr来定义空指针:
//下面两个函数构成函数重载
void f(int x)
{
cout << "f(int x)" << endl;
}
void f(int* ptr)
{
cout << "f(int* ptr)" << endl;
}
int main()
{
//f(0);//调用第一个
//f(NULL);//调用第二个
f(nullptr);//在c++中使用nullptr来定义空指针可以避免类型转换的问题
return 0;
}