前言
今天7-3,放假了,决定重新整理C++的知识点。加油!!!
C语言是结构化和模块化的语言,适合处理较小规模的程序。
对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。
为了解决软件危机, 20世纪80年代, 计算机界提出了OOP(objectoriented programming:面向对象)思想,支持面向对象的程序设计语言应运而生。
发展史
1979年,贝尔实验室的本贾尼等人试图分析unix内核的时候,试图将内核模块化,于是在C语言的基础上进行扩展,增加了类的机制,完成了一个可以运行的预处理程序,称之为C with classes。
1982年出现OOP(objectoriented programming:面向对象)
C++98 C++标准第一个版本 引入STL
C++11
C++&Java
1.C++语法来说,学起来比较繁杂,比Java难
2.C++整体学起来难度和Java基本相当(C++对底层细节要求多,缺陷略多,Java多线程,JVM,GC,EE框架等都比较复杂)
学习路线
写博客 + 思维导图
1.C++关键字
c++63个关键字
c语言32个关键字
2.命名空间
在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。
使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
命名冲突
c语言规定的时变量名不能和关键字冲突,并没有规定不能和库函数的函数名相同,但当我们包含相关头文件时,就会产生冲突。
同一个域里面不能有同名变量。
#include <stdio.h>
int main(int argc, char const *argv[])
{
//命名冲突
//c语言库里面,有2个函数用了scanf 和strlen名称
int scanf = 0;
int strlen = 10;
//变量定义时能通过,但下面调用scanf时就不行,调用scanf是就近读取(局部)
//C语言解决不了这个冲突问题
scanf("%d ",&scanf);
return 0;
}
利用 namespace 去解决
关于%p %x 区别 👉[%p %x 区别](
namespace
定义一个域,包含一些变量,函数,结构体,类等
解决c的命名冲突问题
namespace yzq
{
int scanf = 0;
int strlen = 10;
}
int main(int argc, char const *argv[])
{
//默认访问的是局部或全局
printf("%x\n", scanf); // 2d312e90 结果就是scanf函数的地址
// 搜索scanf时是就近原则,不会到namespace里面去搜索,就近找不到就去全局搜索,头文件里找
//指定访问yzq空间中的
printf("%x\n",yzq::scanf); //0 :: 是域作用限定符
return 0;
}
域
:: 是域作用限定符
左边如果是是空白,默认表示访问的是全局域。
- 全局域
- 局部域
- 命名空间域
- 类域
int a = 0;
int main(int argc, char const* argv[])
{
int a = 1;
printf("%d\n", a); // 局部优先 1
printf("%d\n", ::a); // 默认访问全局 0
return 0;
}
namespace里面可以定义函数,也可以嵌套namespace。
同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
namespace N2
{
int scanf = 10;
int strlen = 20;
int Add(int left, int right)
{
return left + right;
}
// 嵌套定义命名空间
namespace N3
{
int c;
int d;
int Sub(int left, int right)
{
return left - right;
}
// 还可以再套
}
}
namespace也可以放到其他文件里面。
注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中。
命令空间中的变量还是全局变量,命名空间不影响生命周期。
命名空间不能定义在函数内部。
使用方式
1.指定命名空间
#include <iostream>
#include <windows.h>
//C++库为了防止命名冲突,把自己库里面定义的东西都定义在一个std的命名空间中
//使用标准库中的东西 3种方式
//1.指定命名空间 不使用 using namespace std;
int main(int argc, char const *argv[])
{
std::cout << "hello world" << std::endl;
system("pause");
return 0;
}
//写起来很麻烦,每个地方都要用,虽然是最规范的
2.整个展开
//2.把std整个展开,相当于库里面的东西都到全局域了
//看起来方便了,但如果是我们自己定义的东西跟库冲突了就没法解决
//规范的工程项目中是不推荐这种方式的 日常练习比较方便
using namespace std;
int main(int argc, char const *argv[])
{
cout << "hello world" << endl;
system("pause");
return 0;
}
3.部分展开
全放会有冲突风险,不放要全写出来又很麻烦。
//3.对部分常用的库里的东西展开
//折中方案,项目中也经常用
#include <iostream>
using std::cout;
int main()
{
cout << "hello world" << std::endl;
cout << "hello world" << std::endl;
cout << "hello world" << std::endl;
cout << "hello world" << std::endl;
return 0;
}
3.输入输出
关于头文件是否带有 .h
早期标准库将所有功能在全局域中实现,声明在 .h 后缀的头文件中,使用时只需包含对应头文件即可,
后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头文件不带 .h ;
旧编译器(vc 6.0)中还支持 <iostream.h> 格式,后续编译器已不支持,因此推荐使用 iostream +std 的方式。
#include<iostream.h> // 老一点的C++标准用的这个 VC6.0可以用,没有std命名空间
//#include <assert.h> // 为了兼容c
#include <cassert> // cpp包含到命名空间
cin cout
ostream 类型全局对象 cout
istream 类型全局对象 cin
endl 全局的换行符号
#include <iostream>
using namespace std;
int main(int argc, char const* argv[])
{
cout << "hello world" << endl;
// 对比C语言printf scanf 优势: 自动识别类型
int a = 10;
int* p = &a;
// 自动识别类型原理:函数重载+运算符重载
cout << a << "," << p << endl;
char str[10];
// >>流提取运算符
cin >> a;
cin >> str;
// <<流插入运算符
cout << a << str << endl;
return 0;
}
struct Person p = {"小李", 10};
printf("name:%s\n age:%d\n", p.name,p.age); // 格式化输出时用printf更舒服
cout<<"name:"<<p.name<<endl<<"age:"<<p.age<<endl;
// 综合来说,混着用,哪个好用就用哪个
4.缺省参数
声明或定义函数时为函数的参数指定一个默认值,调用该函数时,如果没有指定实参就采用该默认值,否则使用指定的实参。
void TestFun(int a = 0)
{
cout<<a<<endl;
}
int main(int argc, char const *argv[])
{
TestFun(10); // 10
TestFun(); // 0
return 0;
}
全缺省参数:全部参数都有默认值。
半缺省参数:部分参数有默认值,要求必须是从右往左缺省(即从右往左给定默认值),并且是连续的。
因为传参是从左往右传的。
void TestFun(int a = 0, int b = 10, int c = 20); // 全缺省参数
void TestFun(int a, int b = 10, int c = 20); // 半缺省参数
void TestFun(int a = 10, int b = 20, int c = 30)
{
cout << a << endl;
cout << b << endl;
cout << c << endl
<< endl;
}
// 多种调用方式
int main(int argc, char const *argv[])
{
TestFun(1, 2, 3);
TestFun();
TestFun(1);
TestFun(1, 2);
return 0;
}
缺省参数的意义
缺省参数有啥作业呢?
缺省参数使得函数调用更灵活。
举个例子:👇
struct Stack
{
int *a;
int size;
int capacity;
};
void StackInit(struct Stack *ps, int InitCapacity = 4)
{
assert(ps);
ps->a = (int *)malloc(sizeof(int) * InitCapacity);
assert(ps->a);
ps->size = 0;
ps->capacity = InitCapacity;
}
int main(int argc, char const *argv[])
{
struct Stack st1;
//假设知道栈里面存100个数据
StackInit(&st1, 100);
struct Stack st2;
//假设知道栈里面存10个数据
StackInit(&st1, 10);
struct Stack st3;
//假设不知道栈里可能要存多少数据
StackInit(&st3);
system("pause");
return 0;
}
注意
缺省参数不能在定义和声明时同时出现(声明时给就行)
// a.h void TestFunc(int a = 10); // a.c void TestFunc(int a = 20) {} // 注意:如果声明与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用哪个个缺省值。
如果定义时给了声明时没给呢?
// .h #include <iostream> #include <cassert> // cpp包含到命名空间 using namespace std; void QueueInit(int n); // .cpp #include "Queue.h" void QueueInit(int n = 10) { cout << n << endl; } // test.cpp #include "Queue.h" // 编译时 .h 在此处展开了 int main() { // 声明时没给缺省值,定义时给了。 // 链接时才去找函数地址,上面只有一个没有缺省值的QueueInit函数的声明 // 就必须得传参数 QueueInit(); //err QueueInit(20); return 0; }
声明时给了缺省参数,定义不是给缺省参数是可以的。(也是这么要求的,不能同时给!)
半缺省参数必须从右往左依次来给出,不能间隔着给
缺省值必须是常量或者全局变量
C语言不支持(编译器不支持)
5.函数重载
概念
函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数或类型或顺序)必须不同,常用来处理实现功能类似数据类型不同的问题。
C++允许定义同名函数,只需要参数不同(类型或个数不同)。
注意:返回值不同无法构成重载。
int Add(int left = 10, int right = 20)
{
return left + right;
}
char Add(char left, char right)
{
return left + right;
}
double Add(double left, double right)
{
return left + right;
}
double Add(long left, long right)
{
return left + right;
}
int main()
{
cout << Add() << endl;// 30
cout << Add(1, 2) << endl; // 3 字面量 给的整形默认算常量
cout << Add('1', '2') << endl;// c 49 + 50 --> 99 也就是'c'
cout << Add(1.1, 2.2) << endl;// 3.3
cout << Add(10L, 20L) << endl;// 后面加上L表示long类型
return 0;
}
调用时小心陷阱
void f(int a, int b, int c = 1)
{
;
}
void f(int a, int b)
{
;
}
f(1,2,3);
f(1,2); //编译通不过 产生歧义
重载的好处
如果想交换不同类型的变量呢?
// 函数重载的妙:
// 如果是c语言,就得写好几个swap
void swap(int* a, int* b)
{
// ...
}
void swap(double* a, double* b)
{
// ...
}
为什么C语言不支持重载?C++支持?C++底层是如何支持重载的?
1.函数重载原理
编译链接过程:
f.h f.cpp main.cpp
// main.cpp
int main()
{
f(1, 2.222);
f(2.222, 1);
return 0;
}
// f.h
void f(int a, double b);
void f(double a, int b);
// f.cpp
void f(int a, double b)
{
printf("int,double\n");
}
void f(double a, int b)
{
printf("double,int\n");
}
- 预处理 — 头文件展开 + 宏替换 + 去掉注释 + 条件编译
f.i main.i- 编译 — 检查语法,生成汇编代码
f.s main.s- 汇编 — 把汇编代码转成二进制机器码,形成符号表。(符号表里生成有函数名和函数地址映射)
f.o main.o
f.o里面有f函数的符号表,main.o 里面有main函数的符号表。- 链接 — 链接对应的库,找调用函数的地址,链接到一起生成可执行程序。
a.out
函数调用就是去call 函数的地址,但在 main.cpp 里面找不到 add 函数的地址,因为只包含了 f.h 文件 里面只有函数的声明,声明没有函数的地址。
有了声明,编译阶段就允许通过了,编译器会认为函数定义在其他地方,后续链接时再去找函数的定义。
每个目标文件会生成一个符号表 链接时就能找到函数地址,这也正是符号表的意义之一。
链接时如果找不到就会报错
C语言就是拿add做函数名称,因此就没法做到重载。
C++ 通过函数名修饰规则支持了函数重载。
3表示函数名的长度 add就是函数名 ii 表示2个int dd表示2个double
通过函数名修饰规则就能通过 call 调用相应函数地址。
C语言直接拿函数名作为地址。
总结:
归根到底,还是因为C编译器和C++编译器对函数名的修饰不同。在g++下的修饰规则是:(Linux系统下修饰规则)
[ _Z+函数长度+函数名+类型首字母 ]
注意:Linux编译器不区分文件名后缀,因此g++编译器可以编译cpp文件,gcc编译器也可以编译cpp文件。
g++ -o tcpp f.cpp test.cpp
gcc -o tc f.cpp test.cpp
这其实也告诉我们为什么函数的返回类型不同,不会构成函数重载,因为修饰规则并不会受返回值的影响。
2.extern ‘C’的作用
由前面的函数重载原理剖析我们已经知道,链接时会去找相应符号表里找调用函数的地址,除此之外,链接时还会链接静态库或动态库。
C++既然兼容C的库,为什么不能直接调用C的库呢?
还是函数名修饰规则的原因,C++项目链接时去找相关函数的地址,按照C++的函数名修饰规则去找,找的是 [ _Z+函数长度+函数名+类型首字母 ] ,而 C库里面提供的就只是函数的名称作为地址,因此是找不到的。
那C++该如何调用C库呢?
举个例子👇
C++调C库
先建立一个Stack_c静态库:
然后选择生成解决方案,可以在相应文件夹里看到.lib文件了。
现在有一个C++的项目,想用那边的静态库,怎么用呢?
当然是包头文件啦,但是C++项目里面没有Stack相关头文件,因此需要用相对路径包含。
通过相对路径引入头文件后,在主函数内调用isValid,编译可以通过,但是链接不上。
因此还需要配置一下附加库项目和依赖项。
此时再点击生成解决方案,发现还是链接不上,为什么呢?不要着急因为现在就相当于是C++的项目去调用C的静态库,正常情况自然不能链接成功啦。
当然,我们只要将Stack.c 改成Stack.cpp 那么就能链接成功了。
C++项目调用C++库自然是可以的,就像C项目也能正常调用C库。
我们一开始 include 了Stack.h 也就是项目中有了Stack相关函数的声明,通过Stack_c 生成的静态库 .lib 文件里面有符号表,符号表里面有Stack相关函数的地址,进而就能找到Stack相关函数的定义了。
如果非想用C++ 调用C库呢?
这时候 extern “C” 就起作用啦!
extern "C"概念
有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern “C”,意思是告诉编译器,将该函数按照C语言规则来编译
这样C程序可以调用,C++程序也可以调用(C++兼容C)。
回到上面举的例子:
再举个工程中实际的的例子:👇
tcmalloc 是 google 用 C++ 实现的一个项目,他提供 tcmallc() 和 tcfree 两个接口来使用,但如果是C项目就没办法使用,那么他就使用 extern “C” 来解决。
extern "C" void* tcmalloc(unsigned int n); 注意用了 extern "C" 就没法用重载了
谷歌的一个开源项目:
C调用C++库
那问题来了,C如何调用C++的库呢?
难道用 extern “C++” 吗? 注意没有 extern “C++” , 只有 extern “C”
先建立 Stack_CPP 静态库,同上。
依旧是把项目的配置类型改为静态库,然后生成解决方案,在相应Debug文件中就能看到 Stack_CPP.lib 文件
然后新建一个C的项目,注意,C里面是没有 extern “C” 的。还是通过相对路径include C库里面的Stack.h 文件,然后再配置连接器里的附加库目录,和附加依赖项。
再次生成解决方案,同样是无法成功链接的,毕竟现在是C项目调用C++静态库。
C的项目链接时找函数的地址,是通过函数的名字作为地址去C++库里面找的,但C++库里函数名字修饰规则不一样,自然是找不到的。C里面可没有 extern “C++” ,因此只能在C++里面动手了。
在函数声明前加上 extern “C”,告诉编译器,按C的规则去修饰这些函数,C++的静态库可以通过了。
但此时C项目中调用又通不过了。
C中头文件展开出问题了!
头文件展开时是直接把整个 .h 文件中的内容复制过来的,但是不可避免的就会在C的项目中出现了,extern “C”,自然通不过了。
此时我们可以借鉴Google那个开源项目的处理技巧,利用条件编译来处理。
此时C项目就能调用C++的库了。
另一种写法。
再问一个问题,此时能否实现函数重载呢?
当然不能啦!用C的函数名修饰规则找函数地址,如果还有函数重载的话,找到的都是一样的名字,无法区分。
总结
-
C++调用C库
// 告诉编译器,extern "C" 声明的函数是C库,要用C的方式去调用。 extern "C" { #include "../5-4Stack_c/Stack.h" }
-
C调用C++库
#ifdef __cplusplus #define EXTERN_C extern "C" // 识别到c++文件就用 extern "C" 替换 EXTERN_C #else #define EXTERN_C // 否则就用空替换 EXTERN_C #endif // __cplusplus // 告诉编译器,按C的规则去修饰这些函数 EXTERN_C void StackInit(Stack * pst);
6.引用
6.1引用概念
引用其实就是给已经存在的变量取一个别名,编译器不会为引用变量开辟内存空间。
他们共用同一块内存空间。
类型& 引用变量名(对象名) = 引用实体;
注意:引用类型必须和引用实体是同种类型的
有了引用,再写交换函数就不用传地址,直接传引用。看起来岂不是舒服很多?
#include <iostream>
using namespace std;
void Swap(int& r1, int& r2)
{
int tmp = r1;
r1 = r2;
r2 = tmp;
}
int main()
{
int x = 1, y = 2;
cout << x << " " << y << endl;
Swap(x, y);
cout << x << " " << y << endl;
return 0;
}
6.2引用性质
- 一个变量可以有多个别名
int main(int argc, char const *argv[])
{
int a = 10;
// b是a的引用(别名)
int &b = a;
b = 20;
int* p = &b; // p指向b,也就是p里面是b的地址,也就是a的地址。
//还可以继续取别名
int &c = b;
c = 30;
return 0;
}
// a b c地址相同的,也就是改变b同时也就改变了a,c
地址相同:
- 引用必须初始化
//正确示例
int a = 10;
int& b = a;//引用在定义时必须初始化
//错误示例
int a = 10;
int &b;//定义时未初始化
b = a;
- 引用一旦引用一个实体,再不能引用其他实体
也就是别名一旦被定义,后面就不能再改了。
但指针不同,指针的指向是可以改变的。
int a = 10;
int& b = a; // b是a的引用
int e = 20;
b = e;//你的想法:让b转而引用e ‘错误’ 这里只是把e的值赋值给b
- 引用与重载
void Swap(int &r1, int &r2)
{
int tmp = r1;
r1 = r2;
r2 = tmp;
}
int main(int argc, char const *argv[])
{
int a = 10;
int b = 20;
Swap(a, b);
return 0;
}
//int &r1 类型是int
//void Swap(int r1, int r2); 这样不构成重载了
6.3常引用
指针有被const修饰的版本,那引用也应该有吧?当然可以!
取别名的原则:
对原引用变量,读写权限只能缩小,不能放大。
1.权限放大
const int a = 10;
//int& ra = a; // 该语句编译时会出错,a为常量 ra引用a属于权限放大
// a被const修饰,表示a是只读的,如果 int& ra = a; 修饰,就变成可读可写的了。
const int& ra = a; //常引用 权限不变
2.权限缩小
int b = 10;
int& rb = b; // 权限不变
const int& crb = b;// crb引用b属于权限缩小,本来可读可写变成只读的了。
3.隐式类型转换
C++沿袭了C语言的部分特性,因此在相似类型发生操作时会发生隐式类型转换或整型提升。也就是大给小会截断,小给大会提升。
关于隐式类型转换可以看这篇文章。👉戳我
给常量取别名
注意:不能直接给常量取别名,需要用const修饰,常量是只读的。
//int& b = 20; // err
const int& b = 20; // 这样才行
临时变量具有常性
double d = 2.2;
//int& e = d; //err: double 不能转换成 int
// 加个const为什么就可以了呢?
const int& e = d;
double转int
double d = 2.2;
int f = d; // 这里也是把d的整数部分截断,赋值给一个临时变量,临时变量再赋值给f
const int& e = d;
// 发生了隐式类型转换,把d的整数部分截出来给一个临时变量,临时变量再赋给f,而临时变量具有常性
// 也就是,e其实引用的是临时变量,所以加const才能正常引用
// 也就是说,e其实不是d的引用,e只是那个临时变量的别名,e 和 d 地址不一样的。
再举个例子,int 转 double:
int c = 10;
double d = 1.11;
d = c; //会发生隐式类型转换
//借助一个double的临时变量 临时变量具有常性
//double& rc = c; //错误
const double& rc = c; //rc引用的是 c的临时变量
//同理,rc并不是c的别名。
应用于传参
传值传参会有拷贝,因此最好都用传引用传参。
传引用传参可以减少拷贝,关于引用传参我们下面再细说。
void func(int& x)
{
}
int main()
{
int a = 10;
int& b = a;
const int& c = 20;
double d = 2.2;
const int& e = d;
func(a);
// func(10);
// func(c);
// func(d); // 隐式类型转换
// func(e);
// 统统传不过去,常量是只读的,而形参只是int& x 会放大权限,形参得用 const int& x
return 0;
}
形参加个const就能正常传参了。
6.4使用场景
1.做参数
1.1 做输出型参数
所谓输出型参数,也就是函数内部返回给函数外部的。形参的传值调用是输入型参数。
传址调用有可能是输出型参数,也有可能是输入型参数。作为输入型参数使用时,一般会用const关键字修饰,表明是只读,不能修改。可参考:👉戳我
以一道OJ题(LeetCode144)举例。
C语言版本下:int* preorderTraversal(struct TreeNode* root, int* returnSize){ }
如果是C++则可以这么写:
int* preorderTraversal(struct TreeNode* root, int& returnSize){ }
再举个例子:为什么
scanf("%d",&a);
需要取a的地址呢?
scanf是去缓冲区提取我们输入的变量然后赋值给a,而a是传给形参的,要让实参和形参一起改变就得传地址了。如果C语言有了引用,就不需要再取a的地址了。
一个经典例子:Swap函数。
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
// 配合函数重载使用更爽
void Swap(double& a, double& b)
{
double tmp = a;
a = b;
b = tmp;
}
int main()
{
int x = 1, y = 2;
double c = 1.1, d = 2.2;
cout << x << " " << y << endl;
cout << c << " " << d << endl;
Swap(x, y); // a是x的别名,b是y的别名
Swap(c, d); // 实际上调用的不是同一个函数
cout << x << " " << y << endl;
cout << c << " " << d << endl;
return 0;
}
1.2 减少拷贝,提高效率
传值传引用传指针效率比较
#include <time.h>
#include <iostream>
using namespace std;
void Swap(int& r1, int& r2)
{
int tmp = r1;
r1 = r2;
r2 = tmp;
}
struct A
{
int a[10000];
};
void TestFunc1(A a)
{
}
void TestFunc2(A& aa)
{
}
void TestFunc3(A* paa)
{
}
void TestRefAndValue()
{
A a;
// 以值作为函数参数
size_t begin1 = clock();
// 把实参拷贝给形参,10000Byte拷贝100000次,
for (size_t i = 0; i < 100000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 传地址
size_t begin3 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc3(&a);
size_t end3 = clock();
// 分别计算函数运行结束后的时间
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
cout << "TestFunc3(A*)-time:" << end3 - begin3 << endl;
}
int main()
{
TestRefAndValue();
return 0;
}
2.做返回值
特殊场景下才行。
传值返回
例1
先举个静态变量的例子👇
int Count()
{
static int n = 0; // 静态变量只会初始化一次。
n++;
// ...
return n;
}
// 静态的局部变量,虽然作用域只在Count里面,但生命周期是全局的。
// n的地址初始化之后地址就不变了。
int main()
{
cout << Count() << endl;
cout << Count() << endl;
cout << Count() << endl;
// 结果为1 2 3 静态变量只会初始化一次。
return 0;
}
传值返回的过程中会产生一个临时变量,也就是会把返回值拷贝给一个临时对象。
为什么要借助一个临时变量来操作呢?
更一般的情况下,局部变量 n 出了函数作用域就被销毁了,就不能直接把n给ret。
- 如果对象比较小(4/8byte),那么一般会使用寄存器存储对象的拷贝。
- 如果对象比较大,这个拷贝通常会存在上一个函数的栈帧。
// 更一般的情况:
int& Count()
{
int n = 0;
n++;
// ...
return n;
}
int main()
{
int ret = Count();
// int& ret2 = Count(); // err 证明传回来的是临时变量。
const int& ret2 = Count();
return 0;
}
传引用返回
那加个引用又是啥意思呢?
表示临时变量的类型不是 int 而是 int&
int& Count()
{
int n = 0;
n++;
// ...
return n;
}
int main()
{
int ret = Count();
return 0;
}
int& Count()
{
int n = 0;
n++;
// 证明地址都一样
cout << "&n:" << &n << endl;
// ...
return n;
}
int main()
{
int& ret = Count();
// 此时ret是临时对象的别名,也就是n的别名。
// 他们地址都一样
cout << "ret:" << &ret << endl;
return 0;
}
注意:
这个代码是不正确的。间接的一个野指针访问。
猜一猜输出结果是什么?
还是1吗?还是随机值?
int& Count()
{
int n = 0;
n++;
cout << "&n:" << &n << endl;
// ...
return n;
}
int main()
{
int& ret = Count();
cout << "ret:" << &ret << endl;
cout << ret << endl;
return 0;
}
答案:一切皆有可能,VS编译器下现在是随机值。
有些编译器在函数结束后会清空函数所在的那片栈帧,而有些编译器不会清理,我们所用的MSVC编译器就没有清理。
关于函数栈帧,详情可参考这篇文章:👉戳我
第一次输出是1,第二次输出是随机值。
主要看那块空间有没有被覆盖,我们知道,局部变量的销毁意味着把原来的空间的操作权限还给操作系统,原来的空间还是存在的,最初的数据也是不变的,直到被覆盖。
这也就告诉我们,只有特定场景下才用传引用返回。
如果函数返回时,出了函数作用域,如果返回对象还未还给系统,则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
也就是:我们返回的数据必须是被 static 修饰或者是动态开辟的或者是全局变量等不会随着函数调用的结束而被销毁的数据。
例2
再举个例子加深理解。👇
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
cout << "Add(1, 2) is :" << ret << endl;
Add(3, 4);
cout << "Add(3, 4) is :" << ret << endl;
return 0;
}
// 输出结果是多少呢?
分别是3 和 7
第一次调用Add,ret是c的别名,c为3,因此输出3。
第二次调用Add,c 的值被改为7,ret仍然是c的别名,因此输出7。当然这样写是不对的。
再来:
int& Add(int a, int b)
{
static int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
cout << "Add(1, 2) is :" << ret << endl;
Add(3, 4);
cout << "Add(3, 4) is :" << ret << endl;
return 0;
}
// 输出结果是多少呢?
c被static修饰,跑到静态区去了,只会初始化一次。
每一次调用Add函数就直接return c了
梅开三度:
int& Add(int a, int b)
{
static int c = 0;
c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
cout << "Add(1, 2) is :" << ret << endl;
Add(3, 4);
cout << "Add(3, 4) is :" << ret << endl;
return 0;
}
// 输出结果是多少呢?
不过这样也保证了,c不会因为调用输出语句而被刷新掉。
静态区中的变量不会因为函数结束而被销毁。例如👇
int& Add(int a, int b)
{
int c = 0;
c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(3, 4) is :" << ret << endl;
cout << "Add(3, 4) is :" << ret << endl;
cout << "Add(3, 4) is :" << ret << endl;
cout << "Add(3, 4) is :" << ret << endl;
cout << "Add(3, 4) is :" << ret << endl;
return 0;
}
// 输出结果是多少呢?
那为什么cout << "Add(3, 4) is :" << ret << endl;
就会把空间覆盖了呢?
cout << 流操作
<< 相当于operator<<(ret)
调用函数前要先传参。也就是ret要做这里的参数。ret就是c的别名
传值传引用返回效率比较
struct A
{
int a[100000];
};
A a;//定义的是全局变量
// 值返回
A TestFunc1()
{
return a;
}
// 引用返回
A& TestFunc2()
{
return a;
}
void TestReturnByRefOrValue()
{
// 以值作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
TestReturnByRefOrValue();
return 0;
}
3.总结:
传引用做参数既能充当输出型参数,也能减少拷贝提高效率。
传值返回,会生成一个tmp对象的拷贝。
- 如果对象比较小(4/8byte),那么一般会使用寄存器存储对象的拷贝。
- 如果对象比较大,这个拷贝通常会存在上一个函数的栈帧。
传引用返回,返回的是对象的引用,也就是返回变量的别名。
但要特别注意,返回的数据不能是函数内部创建的普通局部变量,因为在函数内部定义的普通的局部变量会随着函数调用的结束而被销毁。
我们返回的数据必须是被 static 修饰或者是动态开辟的或者是全局变量等不会随着函数调用的结束而被销毁的数据。
6.5指针和引用区别
int main(int argc, char const *argv[])
{
int a = 10;
//语法上,这里给a这块空间取了个别名,没有新开空间
int& ra = a;
ra = 20;
//语法上,这里定义一个pa指针变量,开辟4个字节空间,存储a的地址
int* pa = &a; // int* 表示解引用时访问4个字节的空间
*pa = 20;
// 但从底层的角度而言:他们是一样的实现,也就是他们会转换成一样的汇编代码。
return 0;
}
实际从汇编实现角度,引用的底层也是类似指针存地址的方式处理的。
语法和底层是隔离开的。
指针和引用不同点:
从使用的角度进行对比
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求。
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
- 没有NULL引用,但有NULL指针。
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)。
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。
- 有多级指针,但是没有多级引用。
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理。
- 引用比指针使用起来相对更安全。
int a = 10;
int *pa = &a; //pa指向a
*pa = 20;
int b = 30;
int *&rpa = pa; //rpa是pa的别名
rpa = &b; //pa 指向了b
7.内联函数
用inline修饰的函数,编译时C++编译器会在调用内联函数的地方展开,也就省去了函数压栈的开销,提升程序运行的效率。
栈帧里面会做不少事:
保存寄存器,压参数等。
关于函数栈帧,详情可参考这篇文章:👉戳我
C语言为了小函数避免建立栈帧,提供了宏函数支持,宏会在预处理阶段就展开
既然C语言已经解决了,为什么C++还要提供内联函数呢?
宏函数的缺点?
- 不支持调试(在预处理阶段就展开了)。
- 宏语法复杂,容易出错。
- 没有类型安全的检查。
请你写一个宏函数实现加法:👇
// 实现ADD宏
#define ADD(int x, int y) return x + y;
#define ADD(x, y) return x + y;
#define ADD(x, y) (x + y);
#define ADD(x, y) x + y
#define ADD(x, y) ((x) + (y))
// 究竟哪个写的对呢?
// 当不确定自己写的宏是否正确时,写个调用替换一下看一下是否正确。
// 写几个宏函数的调用场景验证:
int main()
{
ADD(1, 2); // 被替换成 ((1) + (2))
if(ADD(1, 2))
{
}
ADD(1, 2) * 4;
// 防止出现 ADD(1, 2) * 4 ---> (1) + (2) * 4的情况
ADD(1&2, 3|4); // 如果里面的x y不给括号,由于+的优先级比&,|的优先级高,就会出现 1 & 2 + 3 | 4 本意是想先算1&2 结果先算了2+3
}
注意要加括号。
正是因为C语言的宏语法机制容易出错,相应的,inline就出现了,解决了宏函数晦涩难懂的问题。
// C++ 推荐频繁调用的小函数 定义成inline,没有栈帧开销。
inline int Add(int x, int y)
{
return x + y;
}
int main(int argc, char const *argv[])
{
int c = Add(1, 2);
return 0;
}
注意:
在默认的Debug版本下是不会展开的,还能看到call函数的地址。只有在release版本下才能看到展开,但release版本下又不能反汇编查看。
因此我们可以对Debug版本下的inline优化进行处理👇
优化之后:
内联缺陷
能不能所有函数都用inline?
内联函数缺陷?
假设Sort() 100行指令
如果10个地方调用,总计110条指令 100+10个call如果是inline Sort
10个地方调用,总计是1000条指令指令变多意味着: 编译出来的可执行程序变大 --> 安装软件的人体验变差,执行程序内存消耗变多。
注意:
inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长(一般10行左右,具体取决于编译器。)/ 递归的函数不适宜使用作为内联函数。
inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内代码比较长/递归等等,编译器优化时会忽略掉内联。
inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
// F.h #include <iostream> using namespace std; inline void f(int i); // F.cpp #include "F.h" void f(int i) { cout << i << endl; } // main.cpp #include "F.h" int main() { f(10); return 0; } // 链接错误:main.obj : error LINK: 无法解析的外部符号 "void __cdecl f(int)" 该符号在函数 _main 中被引用
C++建议用const替代宏。
8.auto关键字
在早期的C/C++中auto的含义是:使用auto修饰的变量是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它。
在C++11中,标准委员会赋予了auto全新的含义:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
int main(int argc, char const *argv[])
{
int a = 10;
// int b = a;
auto b = a;// 类型声明成auto 可以根据a的类型自动推导b的类型
return 0;
}
注意:
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
auto使用
1.auto与指针和引用结合
int main(int argc, char const *argv[])
{
int x = 10;
auto a = &x;// int*
auto *b = &x;// int* 强制指定了b是一个指针
// auto* b = x; // err 必须给b赋值地址
int& y = x;// y的类型是int
auto c = y;// 推出来的c也是int
auto& d = x;// d的类型是int 但这里强制指定了d是x的引用
// 打印变量类型 返回的是类型的字符串
cout << typeid(x).name() << endl;// int
cout << typeid(y).name() << endl;// int
cout << typeid(a).name() << endl;// int*
cout << typeid(b).name() << endl;// int*
cout << typeid(c).name() << endl;// int
return 0;
}
注意:用auto声明引用时必须加&,否则创建的只是与实体类型相同的普通变量,只不过将其换了个姓名而已。
2.在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}
auto注意事项
1.auto不能做函数的参数和返回值
毕竟,如果auto可以做参数,那还要函数重载干嘛呢?
void TestAuto(auto a)
{
}
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导。
auto TestAuto(int a)
{
return 10;
}
// 这样也是不行的。
auto不能做函数参数和返回值,本质为了防止被滥用,引发各种问题。
试想一下,你要调用一个函数,结果这个函数的参数和返回值都是auto,那究竟该传什么呢?傻了吧。
2.auto不能直接用来声明数组
void TestAuto()
{
int a[] = {1, 2, 3};
auto b[] = {4, 5, 6};
}
3.auto作为类型指示符
为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
4.配合范围for
auto在实际中最常见的优势用法就是跟C++11提供的新式for循环,还有lambda表达式等进行配合使用。
auto的实际应用
类型很长,用auto自动推导
// atuo 的实际应用
int main()
{
std::map<std::string, std::string> dict;
dict["sort"] = "排序";
dict["string"] = "字符串";
std::map<std::string, std::string>::iterator it1 = dict.begin();
// 用auto来写,大幅度缩短代码量
// 类型很长时不想写,可以用auto自动推导
auto it2 = dict.begin();
return 0;
}
配合范围for
int main(int argc, char const *argv[])
{
int array[] = {1, 2, 3, 4, 5};
for (auto &e : array) //必须用引用或者指针, 而 auto e : array 只是把array中的值拷贝给e
e *= 2;
//自动遍历,依次取出array中的元素,赋值给e,直到结束
for (auto e : array)
cout << e << " ";
cout << endl;//4 8 12 16 20
return 0;
}
注意:
auto e : array
只是把 array 中的值拷贝给 e
9.范围for
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。
因此C++11中引入了基于范围的for循环。
for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
int main(int argc, char const *argv[])
{
int array[] = {1, 2, 3, 4, 5};
for (int i = 0; i < sizeof(array) / sizeof(array[0]); i++)
array[i] *= 2;
for (int i = 0; i < sizeof(array) / sizeof(array[0]); i++)
cout << array[i] << " ";
cout << endl;
//范围for C++11新语法遍历,更简单
for (auto &e : array) //必须用引用或者指针, auto e : array 只是把array中的值拷贝给e
e *= 2;
//自动遍历,依次取出array中的元素,赋值给e,直到结束
for (auto e : array)
cout << e << " ";
cout << endl;//4 8 12 16 20
return 0;
}
范围for不是必须写auto,直接指定类型也可以的。
for (int e : arr)
cout << e << " ";
cout << endl;
使用条件
1.for循环迭代的范围必须确定。
对于数组而言,就是数组中第一个元素和最后一个元素的范围;
对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
void TestFor(int array[])
{
for(auto& e : array) //这里的array其实不是数组,数组在传参时会退化成指针。
cout<< e <<endl;
}
2.迭代的对象要实现++和==的操作。
10.指针空值nullptr
在良好的C/C++编程习惯中,在声明一个变量的同时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误。
比如未初始化的指针,如果一个指针没有合法的指向,我们基本都是按如下方式对其进行初始化:
int* p1 = NULL;
int* p2 = 0;
NULL其实是一个宏,在传统的C头文件**(stddef.h)**中可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。
不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦。
例如:👇
#include <iostream>
using namespace std;
void Fun(int p)
{
cout << "Fun(int)" << endl;
}
void Fun(int* p)
{
cout << "Fun(int*)" << endl;
}
int main()
{
Fun(0); // 打印结果为 Fun(int)
Fun(NULL); //打印结果为 Fun(int)
Fun((int*)NULL); //打印结果为 Fun(int*)
return 0;
}
程序本意本意是想通过Fun(NULL)调用指针版本的Fun(int* p)函数,但是由于NULL被定义为0,Fun(NULL)最终调用的是Fun(int p)函数。
注:在C++98中字面常量0,既可以是一个整型数字,也可以是无类型的指针(void*)常量,但编译器默认情况下将其看成是一个整型常量,如果要将其按照指针方式来使用,必须对其进行强制转换。
针对C++98中的问题,C++11引入了关键字nullptr。
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为关键字引入的。
- 在C++11中,sizeof(nullptr)与sizeof((void*)0)所占的字节数相同,大小都为4。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
尾声
🌹🌹🌹
写文不易,如果有帮助烦请点个赞~ 👍👍👍
Thanks♪(・ω・)ノ🌹🌹🌹
😘😘😘
👀👀由于笔者水平有限,在今后的博文中难免会出现错误之处,本人非常希望您如果发现错误,恳请留言批评斧正,希望和大家一起学习,一起进步ヽ( ̄ω ̄( ̄ω ̄〃)ゝ,期待您的留言评论。
附GitHub仓库链接