1.C++的发展史 大致了解一下
C++的起源可以追溯到1979年,当时BjarneStroustrup(本贾尼·斯特劳斯特卢普,这个翻译的名字不 同的地方可能有差异)在贝尔实验室从事计算机科学和软件工程的研究工作。面对项目中复杂的软件开 发任务,特别是模拟和操作系统的开发⼯作,他感受到了现有语言(如C语言)在表达能力、可维护性和可扩展性方面的不足。
1983年,BjarneStroustrup在C语言的基础上添加了面向对象编程的特性,设计出了C++语言的雏形, 此时的C++已经有了类、封装、继承等核心概念,为后来的面向对象编程奠定了基础。这⼀年该语言被正式命名为C++。
在随后的几年中,C++在学术界和工业界的应用逐渐增多。⼀些大学和研究所开始将C++作为教学和研 究的首选语言,而⼀些公司也开始在产品开发中尝试使用C++。这⼀时期,C++的标准库和模板等特性 也得到了进⼀步的完善和发展。
C++的标准化⼯作于1989年开始,并成立了⼀个ANSI和ISO(InternationalStandards Organization)国际标准化组织的联合标准化委员会。1994年标准化委员会提出了第⼀个标准化草案。在该草案中,委员会在保持斯特劳斯特卢普最初定义的所有特征的同时,还增加了部分新特征。 在完成C++标准化的第⼀个草案后不久,STL(StandardTemplateLibrary)是惠普实验室开发的⼀系 列软件的统称。它是由AlexanderStepanov、MengLee和DavidRMusser在惠普实验室⼯作时所开发 出来的。在通过了标准化第⼀个草案之后,联合标准化委员会投票并通过了将STL包含到C++标准中的 提议。STL对C++的扩展超出C++的最初定义范围。虽然在标准中增加STL是个很重要的决定,但也因 此延缓了C++标准化的进程。
1997年11⽉14日,联合标准化委员会通过了该标准的最终草案。1998年,C++的ANSI/IS0标准被投入使用。
1.1C++版本更新
- C++的第一个版本是1998年,绝大编译器都支持;
- 如果是一些大公司的项目成熟,现在可能依然还是在用C++11,甚至C++98更老的版本;语法方面用的比较老
- 为什么不用新的版本,用新版本 有些地方会更新,可能导致不兼容,更改起来成本大
1.2. 关于C++23的⼀个小故事
- C++⼀直被诟病的⼀个地方就是⼀直没出网络库(networking),networking之前是在C++23的计划中的,现在C++23已经发布了,但是没有networking,网上引发了⼀系列的吃瓜和吐槽。
- 原本计划的是一个大版本,最后变成了一次小版本更新
- 从C++98开始,原本计划5年更新一次C++的版本,但是一直到C++11才更新,也就说远远不止5年一更;为啥呢? 因为在这个过程中 烂尾了,中间出现一些问题导致迟迟没有更新
- 不过到C++ 11版本后,就是3年一更了,可能是吃了不少教训;
C++23的目标 - 知乎 (zhihu.com)https://zhuanlan.zhihu.com/p/107360459
2. C++参考文档
- 这里有常用的几个C++ 文档,字典
- https://legacy.cplusplus.com/reference/
- https://zh.cppreference.com/w/cpp
- https://en.cppreference.com/w/
- 我最常用的就是第一个,但是只更新到了C++11版本,而且不是官网;
- 第二个才是官网,当然可能很多小伙伴和我一样英语不太好,所以第三个是中文版的官方网站; 当然最好是一起结合使用,会读英语文档更好
3. C++的重要性
3.1编程语言排行榜
- TIOBE排行榜是根据互联网上有经验的程序员、课程和第三方厂商的数量,并使用搜索引擎(如 Google、Bing、Yahoo!)以及Wikipedia、Amazon、YouTube和Baidu(百度)统计出排名数据,只是反映某个编程语言的热门程度,并不能说明一门编程语言好不好,或者一门语言所编写的代码数量多少
2024年6月TIOBE发布的编程语言排行榜 对应的网站TIOBE Index - TIOBE
3.2. C++在工作领域中的应用
C++的应用领域服务器端、游戏(引擎)、机器学习引擎、音视频处理、嵌入式软件、电信设备、金融应用、基础库、操作系统、编译器、基础架构、基础工具、硬件交互等很多方面都有
C++使用度高,也是离不开在各个领域都有使用的,个人觉得得益于C++高效的性能
- 大型系统软件开发。如编译器、数据库、操作系统、浏览器等等
- 音视频处理。常见的音视频开源库和方案有FFmpeg、WebRTC、Mediasoup、ijkplayer,音视频开发最主要的技术栈就是C++。
- PC客⼾端开发。⼀般是开发Windows上的桌面软件,比如WPS之类的,技术栈的话⼀般是C++和QT,QT是⼀个跨平台的C++图形用⼾界面(GraphicalUserInterface,GUI)程序。
- 服务端开发。各种⼤型应用⽹络连接的高并发后台服务。这块Java也比较多,C++主要用于⼀些对性能要求比较高的地方。如:游戏服务、流媒体服务、量化高频交易服务等
- 游戏引擎开发。很多游戏引擎就都是使用C++开发的,游戏开发要掌握C++基础和数据结构,学习图形学知识,掌握游戏引擎和框架,了解引擎实现,引擎源代码可以学习UE4、Cocos2d-x等开源引擎实现
- 嵌入式开发。嵌入式把具有计算能⼒的主控板嵌入到机器装置或者电⼦装置的内部,通过软件能够控制这些装置。比如:智能手环、摄像头、扫地机器⼈、智能音响、门禁系统、⻋载系统等等,粗略⼀点,嵌入式开发主要分为嵌入式应用和嵌入式驱动开发。
- 测试开发/测试。每个公司研发团队,有研发就有测试,测试主要分为测试开发和功能测试,测试开发⼀般是使用⼀些测试⼯具(selenium、Jmeter等),设计测试用例,然后写⼀些脚本进行⾃动化测试,性能测试等,有些还需要⾃行开发⼀些测试用具。功能测试主要是根据产品的功能,设计测试用例,然后⼿动的方式进行测试。
4. C++学习建议和书籍推荐
- C++,算是比较难学的语言,所以要做好准备迎接困难,但是有心人无所畏惧
- 书籍推荐
- C++Primer:主要讲解语法,经典的语法书籍,前后中期都可以看,前期如果⾃学看可能会有点晦涩难懂,能看懂多少看懂多少,就当预习,中后期作为语法字典,非常好用。
- STL源码剖析:主要从底层实现的⻆度结合STL源码,庖丁解⽜式剖析STL的实现,是侯捷⽼师的经典之作。可以很好的帮助我们学习别⼈用语法是如何实现出高效简洁的数据结构和算法代码,如何使用泛型封装等。让我们不再坐井观天,闭门造车,本书课程上⼀半以后,中后期可以看。
- Effctive C++:本书也是侯捷⽼师翻译的,本书有的⼀句评价,把C++程序员分为看过此书的和没看过 此书的。本书主要讲了55个如何正确高效使用C++的条款,建议中后期可以看⼀遍,⼯作1-2年后再看 ⼀遍,相信会有不⼀样的收获。
5. C++的一个程序
- C++是兼容C语言的,vs编译器看到是.cpp就会调⽤C++编译器编译,linux下要用g++编译,不再是gcc
- 所以下列代码可以运行
#include <stdio.h>
int main() //这个也是可以跑起来的,C++ 兼容C
{printf("hello world");return 0;
}
- C++ 是有自己的标准输入输出流的,这里可能不太理解cout 和 endl ,一步一步来后面会解释的
- #include <iostream> //标准输入输出流 这个是C++的标准输入输出的头文件
#include <iostream> //标准输入输出流
using namespace std; // 展开C++ 自带std域
int main()
{int a = 10;int b = 1;b << 1;//在这里是左移操作符,相当于一个符号多用了cout << "hello world" << endl;//C++在这个 << 这个符号叫流输出操作符//endl 可以暂时理解为换行,因为内层挺复杂的,这里只做了解return 0;
}
6. 命名空间
6.1. namespace的价值
- 如果在使用rand变量名的时候,没有#include <stdlib.h> 这个头文件,程序时是可以正常运行的
- 这里rand是一个生成随机数的函数
//#include <stdlib.h>
int rand = 10;
int main()
{ printf("%d", rand);//错误 C2365 “rand”: 重定义;以前的定义是“函数” return 0;
}
- 如果此时加上头文件,会导致 命名冲突
- C++中为了解决这种问题,引入了 namespace 关键字
6.2. namespace的定义
- 定义namespace 命名空间时, 后面跟命名空间的名字 然后加上括号" { } "即可; { }中是命名成员,命名成员的类型可以是定义变量、函数、结构体、类型等等
- 本质上是定义一个 域 ,不用 域 里面的名字可以相同,这样就不冲突了,此时会打印这个函数的地址
#include <stdlib.h>
namespace fight
{int rand = 10;
}
int main()
{ printf("%d\n", rand);printf("%p\n", rand);return 0;
}
- 在不同的域中出现变量a,那么怎么调用到全部的变量呢?: : 域作用限定符
- 这个 : : 符号的左边不写,只写符号右边a,默认访问全局的a
- 在 : : 符号左边加上fight,可以访问到fight域里的a
- 注意:fight域内的成员是全局的 ,只不过通过 namespace 手段,把它们隔离了起来,防止命名冲突
#include <stdlib.h>
namespace fight
{int rand = 10;int a = 100;
}
int a = 30;
int main()
{ int a = 5;printf("%d\n", a);// ::域作用限定符 ::a 默认全局域查找printf("%d\n", ::a);// 访问 fight域的aprintf("%d\n", fight::a);return 0;
}
- C++中域有函数局部域、全局域、命名空间域、类域;域影响的是编译时语法查找⼀个变量、函数、类型出处(声明或定义)的逻辑 ,意思就是在运行这个函数时需要先找到该声明,有了域 就可以隔离起来从而解决命名冲突
- 局部域和全局域会影响查找逻辑(默认情况下查找变量时会先找局部的)如果没找到就会去域里面找,找不到就会报错,还会影响声明周期,但是不会影响命名空间域和类域变量声明周期
- namespace只能定义在全局,当然他还可以嵌套定义;如何解决呢?
通过命名空间域,嵌套解决;你们项目组,其他项目组就不会冲突
namespace jwj
{namespace fight{int rand = 10;int a = 100;int add(int x, int y){return x + y;}struct fg{int n;int size;int* arr;};}namespace gan{int rand = 10;int a = 1145;int add(int x, int y){return x + y;}struct fg{int n;int size;int* arr;};}
}
int main()
{printf("%d\n", jwj::fight::a);printf("%d\n", jwj::gan::a);return 0;
}
- 不同文件,命名空间声明可以相同; 相同名字,会认为是一个namespace, 自己会合并;
直接通过不同的命名空间区分里面相同名字的函数,直接就解决问题了
//Stack.cpp 文件的
namespace jwj
{int Sub(int x, int y){return x * y;}
}
//tset.cpp
namespace jwj
{namespace fight{int rand = 10;int a = 100;int add(int x, int y){return x + y;}struct fg{int n;int size;int* arr;};}namespace gan{int rand = 10;int a = 1145;int add(int x, int y){return x + y;}struct fg{int n;int size;int* arr;};}
}
int main()
{printf("%d\n", jwj::fight::a);printf("%d\n", jwj::gan::a);//这个Sub函数是在Stack.cpp文件中的函数cout << jwj::Sub(5,5) << endl;return 0;
}
- C++标准库都放在一个叫std(standard)的命名空间中。怕和其他名字冲突,所以标准库也做了封装
6.3. 命名空间使用
- 编译器在查找变量声明/定义,默认查找全局和局部的,不会去命名空间查找,从而导致报错;所以此时就有了一下三种方式:
- 指定命名空间访问
- 使用using来展开命名空间的某个成员,对经常使用的成员,当然要确保变量名不冲突
- 使用using展开命名空间中的全部成员,小项目练习可以用。在实际项目中不推荐,风险太大
- 展开头文件和命名空间的区别
- 展开头文件是拷贝一份,而命名空间是把这堵墙拆开(把限制解除了),在这个位置展开
int main()
{using namespace jwj::fight;//展开jwj命名空间里的fight命名空间;也就是展开里面的命名空间printf("%d\n", a);printf("%d\n", a);printf("%d\n", a);using abx::x;//部分展开指的是展开变量、函数、结构体;using abx::add;return 0;
}
7. 输入输出流
- <iostream>, 是InputOutputStream的缩写,是标准的输入、输出流库,定义了标准的输入、输出对象
- std::cout 标准输出流(写入),窄字符的便准输出流;输出到控制台(console)
- std::endl 流插入输出时,相当于插入⼀个换行字符 + 刷新缓冲区;这里也涉及到了运算符重载的
- std::cin 是istream类的对象,窄字符标准输入流(读取)
- <<是流插入运算符,>>是流提取运算符;这是一个 二元操作符,两边都只能有一个操作数 (在c语言中的意思是 << 左移操作符 和 右移操作符 >>)
- 使用C++的输入输出更方便,因为可以自动识别变量类型(本质是通过函数重载实现) C++更好的支持输入输出自定义类型的对象(类)
- IO流涉及类和对象、运算符重载、继承等等;会使用IO流的基本用法就行,后面再深入了解
- cout/cin/endl等都属于C++标准库,都放在一个叫std(standard)的命名空间中,所以要通过命名空间的使用方式 ;std::cerr 输出错误 std::clog 输出日志
- 是日常练习当中,使用using namespace std展开,实际项目中不建议
- 还有一种情况,代码并没有包含<stdio.h>的头文件,但是依然可以用printf和scanf,可能<iostream>间接包含了。但是再gcc环境下要包含<stdio.h>的头文件
#include <iostream> //标准输入输出流
int main()
{using namespace std;//展开 std命名空间 using std::cin;// 展开命名空间里的函数int a = 20;char b = 'b';int c = 40;char arr[10] = "abced";scanf("%d", &c);//iostream 里面间接包含了,所以可以使用printf("%d ", c);cout << a << endl;// cout 窄字符输出流,输出到控制台 a是一个变量,这个会自动转换成字符类型//endl 现阶段可以理解为换行符 + 刷新缓冲区cin >> a >> c >> b >> arr;cout << a << ' ' << c << ' ' << b << ' ' << arr << endl;//流提取操作符return 0;
}
在竞赛时,如果跑不通过,可以加上这三行代码,提高IO效率,减少一些不必要的系统调用
#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;
8. 缺省参数
- 缺省参数时声明或定义函数的参数指定一个缺省值。调用函数时如果没有实参,就会使用缺省值,相反就使用实参,还有全缺省和半缺省;当然也有叫默认参数的
- 全缺省就是全部实参都给缺省值,半缺省就是部分给缺省值,而半缺省值有规定,必须从右向左连续给缺省值,不能跳着给缺省值
- 带缺省值的函数调用,C++规定必须从左到右依次给实参,也不可以跳跃给实参
- 函数的声明和定义分离时,缺省参数不能同时出现(看下列图),规定必须在函数声明给缺省值
#include <iostream>
using namespace std;
void print(int a = 0)
{cout << a << endl;
}
int main()
{print();print(10);return 0;
}
- 全缺省和半缺省
#include <iostream>
using namespace std;
void print1(int a = 1,int b = 2,int c = 3)
{cout << a << " " << b << " " << c << " " << endl;
}
void print2(int a, int b = 2, int c = 3)
{cout << a << " " << b << " " << c << " " << endl;
}
int main()
{//全缺省print1();print1(10);print1(10,20);print1(10,20,30);cout << endl;//半缺省print2(11);print2(11,22);return 0;
}
- 错误示范,调用带有缺省参数的函数,不能跳着给参数,也不可以跳着给缺省值
9. 函数重载
- C++支持同一个作用域中出现同名函数,但是要求同名函数的实参类型不同,或者时参数的个数不同;这样C++函数调用表现出了多态行为,使用更加灵活,C语言是不支持同名函数的
- 假设使用一个相加函数,函数重载就可以很好的解决问题
#include <iostream>
using namespace std;
//类型不同
int add(int pa, int pb)
{cout << "add(int pa,int pb)" << endl;return pa + pb;
}
double add(double pa, double pb)
{cout << "add(double pa,double pb)" << endl;return pa + pb;
}
//交换函数
void Swap(int* x, int* y)
{int tmp = *x;*x = *y;*y = tmp;
}
void Swap(char* x, char* y)
{char tmp = *x;*x = *y;*y = tmp;
}
int main()
{add(10, 20);add(10.1, 20.2);char a = 'h';char b = 'g';int al = 10, bl = 20;Swap(&a, &b);Swap(&al, &bl);cout << "a = " << a << '\n' << "b = " << b << endl;cout << "al = " << al << '\n' << "bl = " << bl << endl;return 0;
}
是不是突然绝对C++,很方便
参数个数不同
#include <iostream>
using namespace std;
// 个数不同
void fun1()
{cout << "fun1()" << endl;
}
void fun1(int a)
{cout << "fun1(int a)" << endl;
}
int main()
{fun1();fun1(10);return 0;
}
个数一样,类型顺序不一样
void testnum(char a, int b)
{cout << "testnum(char a, int b)" << endl;
}
void testnum(int a, char b)
{cout << "testnum(int a, char b)" << endl;
}
int main()
{testnum('g', 10);testnum(20, 'j');return 0;
}
错误的
返回值不一致不构成函数重载
#include <iostream>
using namespace std;
// 返回值不一样,不构成函数重载
void fx(int x, int y)
{cout << "fx(int x, int y)" << endl;
}
double fx(int x, int y)
{cout << "fx(int x, int y)" << endl;return 0.0;
}
int main()
{testnum('g', 10);testnum(20, 'j');return 0;
}
个数不一样构成函数重载,但是调用函数会出问题;因为带有缺省参数的函数,可以不用传实参
//1.个数不一样构成函数重载
//2.但是调用函数会出问题;因为带有缺省参数的函数,可以不用传实参
void fen()
{cout << "fen()" << endl;;
}
void fen(int n = 0)
{cout << "fen(int n = 0)" << endl;
}
int main()
{//testnum('g', 10);//testnum(20, 'j');//fen(); // 调用有问题//fen(10);// 可以找到对应函数,并打印return 0;
}
10. 引用
10.1. 引用的概念和定义
- 引用不是重新定义一个变量,而是给这个变量取一个别名,编译器不会为引用变量开辟新空间,依然是和被引用的变量使用同一块内存空间。
- 假如说:鲁迅,有很多其他的名字,可以叫周树人、周樟寿等等,本质上都是说的同一个人
- 基本结构 : 类型& 引用别名=引用对象;
- C++中为了避免引入太多的符号,所以和取地址使用的同一个符号 ' & ';可以发现C++也使用了C语言中的左移和右移操作符
- 可以看下面例子,语法层面来说就是给a变量取另外一个名字,而且都是指向同一块内存空间的,引用可以改变内容,不能改变指向
#include <iostream>
using namespace std;
int main()
{int a = 10;int& b = a;//给别名取别名int& c = b;int& d = c;cout << "a = " << a << endl;cout << "b = " << b << endl;cout << "c = " << c << endl;cout << "d = " << d << endl;//取别名的地址,它们都指向同一块空间cout << "a = " << &a << endl;cout << "b = " << &b << endl;cout << "c = " << &c << endl;cout << "d = " << &d << endl;return 0;
}
10.2.引用的特性
- 引用在定义是必须初始化
- 一个变量可以有很多个引用
- 引用了一个实体,不能再引用其他实体
#include <iostream>
using namespace std;
int main()
{int a = 10,b = 20;//int& at; 错误的int& ay = a;//一个变量多个引用,本质上都是a的其他名字int& aa = a;int& ab = aa;int& ac = ab;int& ad = a;//只能改变内容,不能改变指向ay = b;cout << ay << endl;return 0;
}
10.3引用的使用
- 引用在实践中,主要是引用传参和引用做返回值,减少拷贝和改变引用对象时同时改变被引用对向;
- 引用和指针功能类似,但是引用更加方便
- 引用的返回值的场景比较复杂,下面场景就很好的说明的用处
#include <iostream>
#include <cassert>
using namespace std;
typedef int STDataType;
typedef struct Stack
{STDataType* a;int top;int capacity;
}ST;
void STInit(ST& rs, int n = 4)
{rs.a = (STDataType*)malloc(n * sizeof(STDataType));rs.top = 0;rs.capacity = n;
}
// 栈顶
void STPush(ST& rs, STDataType x)
{// 满了,扩容if (rs.top == rs.capacity){printf("扩容\n");int newcapacity = rs.capacity == 0 ? 4 : rs.capacity * 2;STDataType* tmp = (STDataType*)realloc(rs.a, newcapacity *sizeof(STDataType));if (tmp == NULL){perror("realloc fail");return;}rs.a = tmp;rs.capacity = newcapacity;}rs.a[rs.top - 1] = x;rs.top++;
}
//以前使用的方法
void Modify(ST& rs, STDataType x)
{rs.a[rs.top - 1] = x;
}
//现在使用的方法
// int STTop(ST& rs)
int& STTop(ST& rs)
{assert(rs.top > 0);return rs.a[rs.top - 1];
}
//int STTop(ST& rs)
//{
// assert(rs.top > 0);
// return rs.a[rs.top - 1];
//}
int main()
{ //调⽤全局的ST st1;STInit(st1);STPush(st1, 1);STPush(st1, 2);STTop(st1) = 5;return 0;
}
- 引用和指针在实践过程中相辅相成,功能相似,但是各不一样,也不可以相互替代。和其他语言有很大的差距,比如java,java和C语言的指针很像可以改变指向,而C++的引用不行
- 引用场景还有一些,看下面代码的函数传参,原本时用来接收一级指针的;现在给链表的结点一级指针重新取了一个别名,pphead;避免了二级指针的繁琐操作
typedef int SLDataType;
typedef struct SListNode
{int data;struct SListNode* next;
}SLNode,*LNode;
// *LNode 等价于 typedef struct SListNode *LNode;
//链表尾删
//void SLDelBack(SLNode** pphead)
//void SLDelBack(SLNode*& pphead)
void SLDelBack(LNode& pphead)
{assert(pphead);if ((pphead)->next == NULL){free(pphead);pphead = NULL;}else{SLNode* ptail = pphead;SLNode* pcur = pphead;while (ptail->next){pcur = ptail;ptail = ptail->next;}free(ptail);ptail = NULL;pcur->next = NULL;}
10.4. const引用
- 可以引用一个const对象,但是也必须用const引用,const也可以引用一个普通对象;但是引用的权限只可以缩小不可以放大
- 比如const int& sum = (a + b); 和 const int& fi = f;这些计算结果和类型转换会产生临时对象空间,而C++中临时空间具有常属性,有常属性就需要const,让它变成只读
- 产生这些为命名的临时空间,可以叫做临时对象
- 类型转换、整型提升、临时计算的结果等等,都是具有常性
using namespace std;
typedef int STDataType;
int main()
{const int a = 10;// 具有了常属性int ai = a;//可以读取,不是权限放大//int& ay = a;//此时会造成权限放大const int& ay = a;//正确做法int b = 20;const int& by = b;//引用时的权限可以缩小,但是不能放大b++;//by++;//const 限制的是by权限,并不限制b,所以可以b++;//int& n = 40;//40具有常属性,会开辟临时空间权限会放大,所以需要const使权限缩小,变成只读const int& n = 40;//n不销毁,40也不会销毁,始终跟着n的生命周期来const int& sum = (a + b);//计算出的结果会产生临时变量,所以也需要constdouble f = 3.15;int ft = f;//发生截断cout << ft << endl;//int& fi = f;//类型转换也会创建临时变量所以需要constconst int& fi = f;double& rf = f;return 0;
}
10.5. 指针和引用的关系
- 指针和引用有着很大的关系,但是互相不替代各有提点,在实践过程中往往一起结合使用解决问题
- 引用不需要开额外的空间,而指针存储一个变量是需要开空间的
- 引用必须初始化,但是指针可以初始化为NULL,或者初始化
- 引用不可以改变指向,一旦引用了这个对象那就不能有其他的对象;为什么呢?
- 可以理解为定义一个指针,它可以通过取地址改变去指向不同的空间,而引用一旦引用了这块空间就不会换,可以更改里面的内容和改变指向 要区分开
- 而指针就可以随意改变指向对象
- 引用可以直接访问对象,而指针需要解引用才能访问对象
- sizeof中含义不同,引用的大小为引用类型的大小,指针在32位平台下占4字节,63位平台下占8字节
- 个人觉得引用比指针更好用,不容易犯错
#include <iostream>
using namespace std;
int main()
{int a = 10;int* ap = &a;*ap = 1;int& ay = a;ay = 3; //间接空引用int* pt = NULL;int& pty = *pt;return 0;
}
11. inline 内联函数
- 内联函数就是用来替代C语言的宏函数,因为C语言的宏很容易出错,不够方便
- 加了inline只是对编译器的一个建议,可以在调用的时候选择不展开,展不展开取决于编译器,就好比如你是一个递归函数,就算加了inline编译器会选择不展开;所以inline适合频繁调用的短小函数
- 个人理解:inline 只是对编译器的一个建议,如果编译器接受你的建议是会展开的(展开减少了函数栈帧的开辟,提高了效率),如果编译器不接受你的建议(代表这个不是内联了),那么就不会展开;此时的编译器还是会认为它是一个函数,是函数就会有栈帧的开辟
- vs编译器debug版本下面默认是不展开inline的,这样方便调试,宏函数复杂容易出错而且不方便调试,所以inline就是来替代宏函数的
- 如果想看看展开,需要在编译器里设置两个地方
- inline非常不建议声明和定义分别放到头文件和.cpp文件,分离会导致编译错误,因为inline被展开了,没有函数地址,链接会导致报错
- 虽然说vs编译器默认不展开,但是它是默认,而在分两个文件放时不行的
#include <iostream>
using namespace std;//宏函数,常见容易写错的地方
//#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()
{int a = 10, b = 20;int sum = ADD(a, b);cout << sum << endl;//1.为什么不能加分号?,因为宏是将整个替换掉,会导致多一个分号//cout << ADD(30, 40) << endl; 假设加分号#define ADD(x ,y) ((x) + (y)); --> cout << 70; << endl;//2.为什么外面要加括号?,最根本的就是优先级问题,假设不加外括号cout << ADD(30, 40) * 10 << endl; // 替换后 30 + 40 * 10,优先级就就是乘法了//3.为什么要加内括号?,如果 x 是一个表达式且不加内括号 10 & 10 + 40 按位与的优先级是很低的,就会导致先算10 + 40cout << ADD(10 & 10, 40) * 10 << endl;return 0;
}
- 为什么不能加分号?,因为宏是将整个表达式替换掉,会导致多一个分号
- 为什么外面要加括号?,最根本的就是优先级问题
- 为什么要加内括号?,这也是优先级问题
- 在汇编代码层面默认不展开,通过call和jmp跳转到对应函数;
- 可以发现,地址跳转都是都是按照指定地址的
- 如果要展开,可以在设置里面勾选,会直接展开执行过程;但是当内联函数里过多代码,编译器会选择不展开
12. nullptr
- NULL在C语言中实际是一个宏,在C的头文件(stddef.h)中是这么定义的
#ifndef NULL#ifdef __cplusplus#define NULL #else#define NULL #endif
#endif
- 在函数调用传参的过程中,NULL有可能会被当做是0,但是和你预期的函数调用不符合,从下放的调试图就可以看出,系统调用的是void f(int x)函数,并没有去调用void f(int* x);C中被定义为无类型指针(void*)的常量,C语言的定义是void*的指针可以传递给任意类型的指针,这也体现出了C语言留下的小毛病
#include <iostream>
using namespace std;void f(int x)
{cout << "void f(int x)" << endl;
}
void f(int* x)
{cout << "void f(int* x)" << endl;
}
int main()
{f(0);f(NULL);//f((void*)0); //这种C语言的写法不行,C语言的定义是void*的指针可以传递给任意类型的指针,但C++不行return 0;
}
//C++
void* a = NULL;
int* pa = (int*)a;//而C++不行//C 隐式类型转换
void testC()
{void* a = NULL;int* pa = a;//这里有隐式类型转换int p = pa;//而C语言pa指针隐式类型转换成了,int类型,C++ 不行}
- C语言会有隐式类型转换,C++没有,并且C++检查会更加严格;所以为了避免这种问题;C++引入了nullptr,nullptr是一个特殊关键字,一种特殊的字面量,可以传递给任意类型,还可以避免类型转换的问题
- nullptr只能隐式转换成指针类型,而不是像上面C语言中的NULL那样被转换成了int类型,也就说nullptr是指针专用的了。
#include<iostream>
using namespace std;
void fi()
{cout << "world" << endl;
}
int main()
{int* fr = fi();return 0;
}
上面代码,调用 fi 函数时,是不能接收任意指针类型的
总结:
- 很久没有写博客了,今天逼自己一把,还是写了;放假有些懈怠,不过还需继续坚持