)
目录前言1、域2、命名空间3、输入输出4、缺省参数5、函数重载6、引用7、函数参数类别8、auto 关键字9、基于范围的 for 循环10、内联函数11、nullptr前言C的主要优点C作为一门已有40多年历史的编程语言能够在快速变化的计算机领域长期保持重要地位主要归功于以下几个核心优势1. 高性能与高效性接近硬件的性能C代码编译后效率极高几乎可以达到手写汇编的性能零成本抽象高级特性(如模板、RAII)几乎不引入运行时开销精细的内存控制允许手动内存管理避免垃圾回收的不可预测停顿内联和优化编译器可以进行深度优化生成极其高效的机器码2. 多范式支持面向对象编程支持类、继承、多态等OOP特性泛型编程通过模板实现高度灵活的通用代码过程式编程保留C风格的过程化编程能力函数式编程C11后引入lambda、函数对象等函数式特性元编程通过模板元编程在编译期执行复杂计算3. 广泛的应用领域系统/操作系统开发Windows/Linux内核、驱动等游戏开发Unreal、Unity等主流游戏引擎高频交易金融行业的低延迟系统嵌入式系统资源受限的物联网设备科学计算高性能数值计算和模拟图形处理3D渲染、计算机视觉等4. 精细的资源控制确定性的资源管理通过RAII模式保证资源及时释放直接内存访问指针和引用提供底层内存操作能力自定义内存管理可重载new/delete实现特殊内存分配策略位级操作可直接操作硬件寄存器和特定内存位置5. 丰富的标准库和生态系统STL(标准模板库)提供强大的容器、算法和迭代器Boost等高质量库扩展了标准库的功能跨平台支持可在几乎所有硬件平台和操作系统上运行成熟的工具链强大的编译器(如GCC、Clang、MSVC)和调试工具6. 向后兼容与稳定性长期兼容性几十年积累的代码仍可编译运行标准化发展由ISO委员会规范发展稳健可控渐进式改进每三年发布新标准平衡创新与稳定7. 社区与行业支持庞大的开发者社区全球数百万C开发者行业标准地位许多关键系统依赖C实现持续现代化C11/14/17/20/23不断引入现代特性教育价值学习C能深入理解计算机原理C值得吐槽的痛点尽管C功能强大但它确实存在不少让人抓狂的设计缺陷和历史包袱。以下是被开发者广泛吐槽的几个方面1. 语法复杂且不一致操作符重载滥用 既表示位移又用于输出 可能执行任意操作最恼人的解析(Most Vexing Parse)TimeKeeper time_keeper(Timer()); 实际声明函数而非对象初始化方式过多C风格、构造函数、列表初始化、统一初始化等至少5种方式模板错误信息灾难简单的模板错误能产生上百行难以理解的编译器输出2. 历史包袱沉重兼容C带来的问题必须保留C的原始指针、宏、隐式转换等危险特性头文件系统过时#include 是简单的文本替换导致编译缓慢预处理器滥用宏污染命名空间难以调试C20模块才开始改善3. 内存安全问题悬垂指针/引用访问已释放内存是常见错误手动内存管理忘记delete导致内存泄漏或重复delete导致崩溃未定义行为(UB)陷阱数组越界、空指针解引用等行为完全不可预测4. 编译与构建问题编译速度慢模板实例化使编译时间随项目规模指数增长二进制兼容性问题不同编译器甚至同一编译器的不同版本可能不兼容构建系统复杂CMake语法反人类但又是事实标准5. 学习曲线陡峭特性膨胀C11/14/17/20/23不断增加新特性语言规范已超2000页多重范式混乱同一项目可能混杂面向对象、泛型、函数式等多种风格专家级陷阱即使经验丰富的开发者也会踩中UB或模板元编程的坑6. 现代化进程的尴尬新特性采用缓慢许多项目仍停留在C98/03因旧代码迁移成本高半吊子改进比如std::string_view解决了一些问题但依然不是默认字符串类型模块化姗姗来迟C20才引入模块生态迁移需要多年7. 工具链痛点调试困难模板展开后的代码、优化后的二进制难以调试包管理落后没有官方包管理器Conan/vcpkg等第三方方案各有缺陷IDE支持有限由于语言复杂性代码补全和重构不如Java/C#流畅正如Bjarne Stroustrup所说C的设计目标就是让你能轻松射击自己脚部同时让高手能写出优雅高效的代码。1、域域有类域、命名空间域、局部域、全局域等。// 全局域 int a 0; // 命名空间域 namespace yu { int a 1; } int main() { // 局部域 int a 2; return 0; }main 函数中如果要访问 a 变量默认先在局部域访问如果局部域没有定义就在全局域访问如果全局域没有定义并且没有展开命名空间域或指定访问命名空间域编译器不会在命名空间域访问而是会报错。:: 限定域访问符:: 的作用是告诉编译器优先在哪里访问使用的基本形式域 :: 变量名如果 :: 前面什么都没有或只有空格表示在全局域访问如果是命名空间名表示在命名空间内访问。2、命名空间#include stdio.h #include stdlib.h int rand 10; int main() { printf(%d, rand); return 0; }以上程序会报错“rand”: 重定义以前的定义是“函数”原因是 stdlib.h 文件中定义了 rand 函数我们又定义了一个全局变量也叫 rand这就产生了命名冲突的问题。C 采用“命名空间”来解决此类问题。举个例子来说明命名空间如何使用#include stdio.h #include stdlib.h namespace variable { int a 10; } namespace My_namespace { // 命名空间中可以定义变量/函数/类型 int rand 10; int Add(int left, int right) { return left right; } struct Node { struct Node* next; int val; }; } // 命名空间可以嵌套 // test.cpp namespace N1 { int a; int b; int a1 int Add(int left, int right) { return left right; } // N1 中嵌套 N2 namespace N2 { int c; int d; int a1;//与命名空间 N1 的 a1 是可以同时存在的 int Sub(int left, int right) { return left - right; } } } // 3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。 // ps: 一个工程中的test.h和上面test.cpp中两个N1会被合并成一个 namespace N1 { int Mul(int left, int right) { return left * right; } } //全部展开命名空间 using namespace variable; //部分展开命名空间 using N1::b; int main() { printf(%d, a); //访问嵌套的命名空间里的变量 printf(%d,N1::N2::a); //访问命名空间里的函数 printf(%d,N1::Add(1,1)); return 0; } // 可以给命名空间起别名 namespace My_namespace std; uisng namespace My_namespace; // 等价于 uisng namespace std;定义了命名空间 variable 并展开该命名空间。using namespace 的作用是将命名空间包含在全局域。如果全局域也有一个变量叫 a编译器会报错说 a 不明确所以不能随便轻易使用 using 关键字。但可以使用 using 关键字将命名空间部分展开比如说 using std::cout;运用命名空间和限定域访问符就可以解决命名冲突的问题#include stdio.h #include stdlib.h namespace variable { int rand 10; } int main() { // 访问自己定义的 rand printf(%d\n, variable::rand); // 访问库定义的 rand printf(%p\n, rand); return 0; }std 命名空间的使用惯例:std 是 C 标准库的命名空间,如何展开 std 使用更合理呢?1. 在日常练习中,建议直接 using namespace std 即可,这样就很方便。2. using namespace std 展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型/对象/函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模大,就很容易出现。所以建议在项目开发中使用像 std :: cout 这样展开常用的库对象/类型等方式。3、输入输出需要包含头文件include iostream并且要展开 cout 、 endl、cin 等。 流插入运算符 流提取运算符输出输出字符串cout Hello world endl;在屏幕上就打印了 ”Hello world“cout 可以理解为终端黑框框, 可以理解为”流向“即 ”Hello world“ 流向了终端黑框框endl 是 end line等价于 ‘\n 。输出变量int x 1; double y 1.1; cout x y endl;相对于 printf 来说这种方式可以自动识别变量的类型用相应的方式来输出变量。如果想控制输出变量的小数位数可以使用 c 语言的 printf 而不使用 c 的方式因为 c 语言的方式更方便。输入int a 0; cin a;一次输入多个变量int a , b , c; cin a b c;在终端中输入 1 2 3 再按回车abc就被赋值为 123.c 语言的 printf 和 scanf 比 c 的效率要更高。getline 函数有时候向string对象输入字符时不想以空格为分割仍以‘\n 为输入结束就可以使用 getline 函数#includeiostream #includestring using namespace std; int main() { string a; getline(cin, a); return 0; }在终端输入 “a b c d a 的内容就是 a b c d。istream 的 get 成员函数istream 类有一个成员函数叫 get 它可以获取包括空格和换行符的输入#includeiostream #includestring using namespace std; int main() { char ch; ch cin.get(); return 0; }但这样就无法结束输入了通常结合循环条件来结束输入#includeiostream #includestring using namespace std; int main() { char ch; string s; ch cin.get(); while (ch ! ch ! \n) { s ch; ch cin.get(); } return 0; }如果循环条件是 while(ch ! \n) 那就是 getline 的模拟实现了。4、缺省参数缺省参数默认参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。void Func(int a 0) { cout a endl; } int main() { // 没有传参时,使用参数的默认值 Func(); // 传参时,使用指定的实参 Func(10); return 0; }缺省参数的分类全缺省参数void Func(int a 10, int b 20, int c 30) { cout a a endl; cout b b endl; cout c endl; }int main() { Func(); Func(1); // 1 传给了 a Func(1,2); // 1 传给了 a2 传给了 b Func(1,2,3); //Func(1,,3) error return 0; }半缺省参数void Func(int a, int b 10, int c 20) { cout a a endl; cout b b endl; cout c c engl endl; }注意:1. 半缺省参数必须从右往左依次来给出,不能间隔着给2. 缺省参数不能在函数声明和定义中同时出现防止函数声明和定义中的缺省参数不一致。缺省参数只能在函数声明中出现因为预处理阶段包含头文件时将函数声明拷贝替换的是函数声明如果缺省参数在函数定义出现那么编译器在检查函数调用时的参数是否合法时会报错缺省参数的应用在初始化栈的时候有时候我们已知数据元素的个数那么我们在初始化时候能不能开辟相应的大小呢是可以的只需要给初始化函数多设置一个形参就行了但这个形参又不是必须接收实参的比如不知道数据元素的个数这时我们不可以随便开辟一定空间因为可能开小了要扩容开大了要浪费空间这时就可以使用缺省参数void StackInit(struct Stack* pst, int defaultCapacity 4) { pst-a (int*)malloc(sizeof(int) * defaultCapacity); if (pst-a NULL) { perror(malloc fail); return; } pst-top 0; pst-capacity defaultCapacity; } int main() { struct Stack st1; // 插入100个数据 StackInit(st1, 100); // 不知道要插入多少数据 struct Stack st2; StackInit(st2); }defaultCapacity 就是一个缺省参数5、函数重载函数重载:是函数的一种特殊情况,C允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表 (参数个数 或 类型 或 类型顺序 或 权限 或 引用类型左值引用还是右值引用) 不同,或者是权限不同常函数和同名非常函数构成重载返回值类型不同不构成重载,常用来处理实现功能类似数据类型不同的问题。#includeiostream using namespace std; // 构成函数重载的前提是在同一作用域 // 1、参数类型不同 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; } // 4、参数的权限不同 void f(int a) { cout f(int a) endl; } void f(const int a) { cout f(int a) endl; } // 5、函数的权限不同 void f(int a) { cout f(int a) endl; } void f(int a) const { cout f(int a) endl; } // 6、参数的引用类型不同 void f(int a) { cout f(int a) endl; } void f(int a) const { cout f(int a) endl; }会发生歧义的函数重载void f() { cout f() endl; } void f(int a 0) { cout f(int a) endl; }按照函数重载的定义以上函数构成重载但如果无参调用会有歧义。c 是如何支持函数重载的呢// Stack.h typedf StackType int; struct Stack { StackType* a; int top; int capacity; }; void StackInit(struct Stack* pst, int defaultCapacity 4) { pst-a (int*)malloc(sizeof(int) * defaultCapacity); if (pst-a NULL) { perror(malloc fail); return; } pst-top 0; pst-capacity defaultCapacity; } void StackPush(struct Stack* pst,int a); void StackPush(struct Stack* pst,double a); // Stack.cpp #include Stack.h void StackPush(struct Stack* pst,int a) { printf(StackPush(struct Stack* pst,int a)); } void StackPush(struct Stack* pst,double a) { printf(StackPush(struct Stack* pst,double a)); } // Test.c #include Stack.h int main { Stack s; StackInit(s); StackPUsh(s,1); StackPUsh(s,1.1); return 0; }编译链接过程回顾在 call 指令后有函数的地址 07B1492h 执行 call 指令后会跳转到 jmp 指令jmp 指令会跳转到函数。那么 StackPush 函数的地址是在什么时候生成的呢在 Test.i 经过编译后生成 Test.s 后函数的地址并没有在 Test.s 中生成而是在 Stack.s 中生成。因为 Test.s 包含的头文件里是函数的声明而不是定义在 Stack.s 里才有函数的定义。Test.i 经过编译后生成 Test.s 的过程中编译器只是检查调用函数时的实参与函数声明函数声明是预处理阶段拷贝过来的这也说明了函数中的缺省参数要在声明中定义中的形参是否匹配函数的地址编译器暂时不管而函数的地址是在链接时确定。如果函数的定义也在 Test.i 那么在编译时直接生成该函数地址。在 c 语言中直接靠函数名来区分函数但在 c 中生成的两个函数名不同C 函数名修饰Name Mangling规则函数名修饰Name Mangling是 C 编译器在编译过程中对函数名进行编码的一种机制目的是为了支持函数重载、命名空间、模板等 C 特性。由于 C 允许函数重载相同函数名不同参数列表编译器需要生成唯一的符号名以便链接器正确识别和链接。1. 为什么需要 Name Mangling在 C 语言中函数名在符号表中就是原始名称如printf但 C 由于支持函数重载void foo(int)和void foo(double)命名空间ns::func类成员函数MyClass::method模板函数std::vectorint::push_back编译器必须对函数名进行编码使其唯一例如void foo(int)→_Z3fooivoid foo(double)→_Z3food2. 不同编译器的修饰规则C 标准没有规定统一的 Name Mangling 规则不同编译器采用不同的方案(1) GCC/Clang (Itanium ABI)采用Itanium C ABI规则Linux/macOS 默认格式_Z 函数名长度 函数名 参数类型编码示例int foo(int); // _Z3fooi float bar(double, int); // _Z3bardf namespace ns { void func(); // _ZN2ns4funcEv } class MyClass { void method(); // _ZN7MyClass6methodEv };(2) MSVC (Microsoft ABI)Windows 平台使用不同的规则格式? 函数名 参数类型 返回类型 其他修饰示例int foo(int); // ?fooYAHHZ float bar(double, int); // ?barYAMHNZ namespace ns { void func(); // ?funcnsYAXXZ } class MyClass { void method(); // ?methodMyClassQEAAXXZ };6、引用引用不是新定义一个变量,而是给已存在的变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。类型 引用变量名(对象名) 引用实体;1. 引用在定义时必须初始化2. 一个变量可以有多个引用3. 引用一旦引用一个实体,再不能引用其他实体void TestRef() { int a 10; int ra a;// 定义引用类型 printf(%p\n, a); printf(%p\n, ra); }注意:引用类型必须和引用实体是同种类型的引用这种语法优化了指针的复杂性:1、Swap 函数不再使用指针Swap 函数现在可以直接传变量而不是变量的地址。void Swap(int a, int b) { int tmp a; a b; b tmp; } // 对比使用指针 void Swap(int* a, int* b) { int tmp *a; *a *b; *b tmp; }2、交换一级指针的指向不再使用二级指针void Swap(int* a, int* b) { int* tmp a; a b; b tmp; } // 对比使用二级指针 void Swap(int** a, int** b) { int* tmp *a; *a *b; *b tmp; }引用的使用场景1. 做输出型参数形参的改变影响到实参的值void Swap(int left, int right) { int temp left; left right; right temp; }2. 做返回值传值返回与传引用返回int Count () { static int n 0; n; // ... return n; }函数返回值时通常将返回的值先存储在寄存器中等退出函数时函数栈帧被销毁此时待返回的值在寄存器中。在返回到的函数栈帧中寄存器的值赋值给调用函数的表达式作为该表达式的值。寄存器的大小通常只有 4 字节。如果返回的值是静态的变量那么该返回值仍会被存储到寄存器里去吗答案是会因为这样可以简化编译器的设计编译器不想考虑多种情况它想作统一处理。那怎么不使用寄存器临时变量呢方法当然是使用引用作为返回值。不使用寄存器临时变量有什么价值如果函数的参数是大对象占用空间很多这样可以减少拷贝提升效率。如果以上代码的 n 不是静态的int Count() { int n 0; n; return n; } int main() { int ret Count(); cout ret endl; return 0; }这里打印 ret 的值是不确定的如果 Count 函数结束,栈帧销毁,没有清理栈帧,那么ret的结果侥幸是正确的。如果 Count 函数结束,栈帧销毁,清理栈帧,那么ret的结果是随机值。int Count() { int n 0; n; return n; } int main() { int ret Count(); cout ret endl; Count(); cout ret endl; return 0; }总结:1、基本任何场景都可以用引用传参。2、谨慎用引用做返回值。出了函数作用域,对象不在,就不能用引用返回,还在就可以用引用返回。引用作返回值的两大功能1、 减少拷贝提升效率2、 获取、修改返回值获取、修改返回值int SLAt(SeqList ps, int pos) { assert(pos 100 pos 0); return s.a[pos]; }以上代码可以获取顺序表第 pos 位置的值并且可以对 pos 位置的值在函数外直接修改//对下标为 0 的位置赋值为 1 SLAt(s, 0) 1; //对下标为 0 的位置增加 5 SLAt(s, 0) 5;如果不使用引用则要先获取第 pos 位置的值SLGet 函数再对 pos 位置进行修改 SLModify 函数常引用在引用过程中权限可以平移或缩小但不能放大。const int a 0; int b a; // 不可以 // 引用过程中,权限不能放大 int x 0; int y x; // 可以 // 引用过程中,权限可以平移或者缩小 const int z x; //缩小 z 作为引用的权限 x;//可以 x此时 z 的值增加 1但不能 z const int a 10;//给常量取别名 double a 1.1 int b a; // 可以隐式类型转换 // 隐式类型转换会产生临时变量临时变量具有常性要用常引用 int b a; // 不可以 const int b a; // 可以不管是隐式类型转换还是显式类型转换都会产生临时变量临时变量具有常性要传递给常引用的函数参数const 类型名上图解释了为什么 double 类型可以取 const int 类型的别名因为临时变量具有常性。int func1() { static int x 0; return x; } int func2() { static int x 0; return x; }func1 函数的返回值不能用 int 类型接收因为函数的返回值具有常性。要用 const int 类型接收func2 函数的返回值可以用 int 类型接收权限平移也可以用 const int 类型接收权限缩小指针的引用在某个类中有以下变量的声明float* cc 是 一个指向 float 类型的指针的引用。引用与指针的区别从底层汇编指令实现的角度看引用是类似指针的方式实现的引用和指针的不同点:1. 引用概念上定义一个变量的别名,指针存储一个变量地址。2. 引用在定义时必须初始化,指针没有要求3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体4. 没有NULL引用,但有NULL指针5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小7. 有多级指针,但是没有多级引用8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理9. 引用比指针使用起来相对更安全和便利引用的底层原理引用的底层原理是指针常量T* const。即引用在底层存储的是对象的地址且这个地址指针本身的值不可修改但可以通过该地址修改所指对象的内容。既然底层是指针为什么C还要发明引用为了安全性和语法的便利性。编译器在编译期会对引用做严格的限制这些限制让引用区别于指针7、函数参数类别1. 输入型参数Input Parameters定义函数/方法只读取参数值不修改原始数据。特点参数值在函数内部是只读的函数不会改变调用者传递的原始数据通常传递值或常量引用void printValue(int x) { // 传值 - 输入参数 cout x; // x 10; // 可以修改但不会影响调用者的变量 } void printString(const string str) { // 常量引用 - 安全的输入参数 cout str; // str.clear(); // 错误不能修改 }2. 输出型参数Output Parameters定义函数将结果写入参数调用者提供变量来接收输出。特点调用时参数通常未初始化或值被忽略函数负责写入有效值常见于需要返回多个值的场景void getMinMax(const vectorint arr, int minVal, int maxVal) { minVal arr[0]; // 写入输出参数 maxVal arr[0]; // 写入输出参数 // ... } // 调用 int min, max; getMinMax(numbers, min, max); // min, max 是输出参数3. 输入输出型参数Input/Output Parameters定义参数既提供输入值又接收修改后的输出值。特点调用时传递已初始化的值函数读取并可能修改该值修改后的值对调用者可见void increment(int x) { // 输入输出参数 x x 1; // 读取并修改 } // 调用 int value 5; increment(value); // value 变为 6约定输入型参数、输出型参数、输入输出型参数的格式为输入型参数const输出型参数*输入输出型参数8、auto 关键字auto 可以根据右边的的表达式自动推导要创建的变量的类型。当变量的类型很长时可以用 auto 简化。使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。auto的使用细则1. auto与指针和引用结合起来使用用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加int main () { int x 10; auto a x; auto* b x; auto c x: return 0; }2. 在同一行定义多个变量当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。void TestAuto () { auto a 1, b 2; auto c 3, d 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同 }auto 不能推导的场景1. auto不能作为函数的参数// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导 void TestAuto(auto a)2. auto不能直接用来声明数组void TestAuto { int a[] {1,2,3}; auto b[] {4, 5, 6}; }9、基于范围的 for 循环基本用法for (auto e : arr) { cout e ; } cout endl;依次取 arr 的元素赋值给 e自动判断结束。由于 e 是 arr 元素的拷贝所以 e 的改变不影响 arr如果要用范围 for 来修改 arr 数组e 必须是arr 元素的引用// 修改数据 for (auto e : arr) { e * 2; }如果 arr 中的每个元素所占空间比较大为了提升效率也可以使用引用。范围for的使用条件for循环迭代的范围必须是确定的对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。注意:以下代码就有问题,因为for的范围不确定void TestFor (int array[]) { for (auto e : array) cout e endl; }范围 for 的底层实现原理是迭代器10、内联函数有些函数需要被频繁的调用调用函数时建立函数栈帧的时间开销很大。#includeiostream using namespace std; int Add(int x, int y) { return (x y) * 10; } int main() { for (int i 0; i 10000; i) { cout Add(i, i 1) endl; } return 0; }上面的 Add 函数要被调用 10000 次就要建立 10000 次栈帧。解决方法1宏函数#define Add(X,Y) (((X) (Y)) * 10)解决方法2内联函数inline int Add(int x, int y) { return (x y) * 10; }inline 关键字使得函数在调用的地方不会生成 call 指令而是被替换为函数的指令。假如有一个函数叫 func 该函数经过编译后有 50 行指令调用函数时只有一句 call 指令如果该函数不是内联函数在一个项目中被调用 10000 次那么总计有 10000 50 行指令如果该函数是内联函数那么总计有 10000 * 50 行指令。指令变多会使软件的安装包变大。inline对于编译器仅仅只是一个建议,最终是否成为inline,编译器自己决定像类似这些函数就算加了 inline 也会被编译器否决1、比较长的函数 2、递归函数inline 关键字在默认 debug 模式下不会生效否则不方便调试。可以设置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 LNK2019: 无法解析的外部符号 //void_cdecl f(int) (? fYAXHZ),该符号在函数 _main 中被引用解决方法是将内联函数的声明和定义都放在头文件中。11、nullptrNULL和nullptr都用于表示空指针但它们在C中有重要区别NULL传统C/C中的空指针常量通常是定义为整数0的宏#define NULL 0类型不明确可以是整数或指针类型可能导致函数重载的不明确调用问题nullptrC11引入的关键字明确的空指针常量类型为std::nullptr_t不会与整数类型混淆提供更好的类型安全void f(int) { cout f(int) endl; } void f(int*) { cout f(int*) endl; } int main() { f(0); f(NULL); return 0; }以上代码中按我们的理解 f(0) 应该调用 f(int) f(NULL) 应该调用 f(int*) 但实际上 f(0) 和 f(NULL) 都是调用的 f(int)。在C98中,字面常量 0 既可以是一个整形数字,也可以是无类型的指针 (void*) 常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转 (void *)0。注意:1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C11作为新关键字引入的。2. 在C11中,sizeof(nullptr)与 sizeof((void*)0)所占的字节数相同。3. 为了提高代码的健壮性,在表示指针空值时建议最好使用nullptr。