![[Pasted image 20260808210409.png]]1. 命名空间#includestdio.h #includestdlib.h // 定义在全局域 int rand 10; int main() { // 编译报错 “rand”: 重定义以前的定义是“函数” printf(%d, rand); return 0; }#includestdio.h #includestdlib.h int main() { // 定义在函数局部域 int rand 1; printf(%d, rand); // 访问全局变量 printf(%d, ::rand); return 0; }编译器查找一个变量/函数/类型的声明或定义时默认逻辑是先查找局部域再查找全局域。在 C 中变量、函数以及类都是大量存在的默认情况下它们的名称都存在于全局作用域中这极易导致命名冲突。因此使用namespace的目的是对这些标识符的名称进行本地化以避免命名冲突和名字污染。1.1. C 中的域C 中常见的域包括局部域 (Local Scope)全局域 (Global Scope)命名空间域 (Namespace Scope)类域 (Class Scope)变量的生命周期• 局部域 全局域不仅影响查找还会决定变量的生命周期如局部自动变量随栈帧创建和销毁。• 命名空间域 类域只影响编译查找不影响变量的生命周期。定义在namespace中的变量生命周期与全局变量一致位于静态存储区贯穿整个程序运行期。1.2.namespace定义命名空间需要使用namespace关键字。命名空间中可以定义变量、函数、类型等。namespace namespace_name { //成员 }namespace的本质是定义一个命名空间域这个域跟全局域各自独立不同的域可以定义同名变量。1.3. 命名空间的使用编译时查找一个变量的定义/声明时默认只会在局部域或全局域中寻找。我们要使用命名空间中定义的变量/函数有以下三种方式指定命名空间访问项目中推荐的方式使用域作用限定符::展开命名空间中的全部成员项目不推荐冲突风险较大使用using namespace指令展开命名空间这样在使用命名空间时就可以不用在前面加上命名空间的名称。using namespace namespace_name;#includeiostream using namespace std; namespace space { int a 0; int b 1; } using namespace space; int main() { cout a endl; return 0; }展开命名空间中的某个成员项目中经常访问的不存在冲突的成员使用使用using指令展开命名空间中的某个成员。using namespace_name::namespace_member_name;#includeiostream using namespace std; namespace space { int a 0; int b 1; } using space::a; int main() { cout a endl; cout space::b endl; return 0; }注意使用using展开时在存在同名变量的情况下若存在局部域变量则局部域变量的优先级最高若仅存在全局域变量与命名空间变量会报错。#includeiostream using namespace std; int a 10; namespace space { int a 0; int b 1; } using namespace space; int main() { // 存在局部变量 int a 20; cout a endl;//输出 20 return 0; }#includeiostream using namespace std; int a 10; namespace space { int a 0; int b 1; } using namespace space; int main() { cout a endl; //编译报错 “a”: 不明确的符号 return 0; }2. 输入 输出iostream是 Input Output Stream 的缩写是标准的输入、输出流库定义了标准的输入、输出对象。注意C 中没有包含stdio.h也可以使⽤printf和scanf在iostream中间接包含了。vs系列编译器是这样的其他编译器可能会报错。在 C 中标准库都放在名为std的命名空间中。std::cin是istream类的全局对象它主要面向窄字符char类型的变量的标准输入流。std::cout是ostream类的全局对象它主要面向窄字符的标准输出流。std::endl是一个函数在流插入输出时相当于插入一个换行符并刷新缓冲区。是流插入运算符是流提取运算符号。C 的输入输出方式相较于 C 语言的scanf和printfC 的输入输出可以自动识别变量类型本质通过函数重载实现并且能够更好的支持自定义类型对象的输入和输出。这是因为在 C 中已经完成了内置类型输入输出的实现并且支持对自定义类型的运算符重载。#includeiostream using namespace std; int main() { int a 0; double b 0.1; char c x; cin a b c; cout a b c endl; return 0; }#includeiostream using namespace std; int main() { // 在io需求⽐较⾼的地⽅如部分⼤量输⼊的竞赛题中加上以下3⾏代码 // 可以提⾼CIO效率 ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); }3. 缺省参数缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时如果没有指定实参则采用该形参的缺省值否则使用指定的实参。缺省参数分为全缺省和半缺省。有些地方把缺省参数也叫默认参数全缺省就是全部形参给缺省值半缺省就是部分形参给缺省值。C 规定半缺省参数必须从右往左依次连续缺省不能间隔跳跃给缺省值。带缺省参数的函数调用C 规定必须从左到右依次给实参不能跳跃给实参。函数声明和定义分离时缺省参数不能在函数声明和定义中同时出现规定必须在函数声明中给缺省值。编译器在看到函数调用的时候只会查看函数声明不会去找函数定义。默认参数实际上不是函数的一部分而是编译器在调用时帮你补上。#includeiostream 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; } void Func2(int a, int b 20, int c 30) { cout a a endl; cout b b endl; cout c c endl; } int main() { Func1(); Func1(1); Func1(1, 2); Func1(1, 2, 3); //不支持Func1(,,3) Func2(1); Func2(1, 2); Func2(1, 2, 3); }例如在栈的实现中我们便可以通过使用缺省参数来简化栈的初始化。Stack.h: #includeiostream #includeassert.h typedef int STDataType; typedef struct Stack { STDataType* arr; int top; int capacity; }ST; void STInit(ST* ps, int n 4); //////////////////////////////////////////////////////////////////////// Stack.cpp: #includeStack.h // 缺省参数不能声明和定义同时给 void STInit(ST* ps, int n) { assert(ps); STDataType* tmp (STDataType*) malloc(n * sizeof(STDataType)); if (tmp NULL) { perror(malloc); exit(1); } ps-arr tmp; ps-top 0; ps-capacity n; } //////////////////////////////////////////////////////////////////////// test.cpp #includeStack.h int main() { // 不知道要开多少空间 ST S1; STInit(s1); // 确定要插入1000个数据初始化时直接开好避免后续扩容提升代码运行效率 ST S2; STInit(s2, 1000); }4. 函数重载C 语言 是不支持同一作用域中出现同名函数的。C 支持在同一作用域中出现同名函数但是要求这些同名函数的形参不同可以是参数个数不同、类型不同或者参数类型顺序不同本质还是类型不同。// 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; }易错点返回值不同不能作为重载条件因为调用时没法区分。// 不够成重载 void fxx() {} int fxx() { return 0; }两个重载函数的调用入口不能重叠// 下面两个函数构成重载但是调用时会报错 // 在不输入参数的情况下存在歧义编译器不知道调用谁 void f1() { cout f() endl; } void f1(int a 10) { cout f(int a 10) endl; }// 在输入参数的情况下存在歧义编译器不知道调用谁 void f1(int a 10) { cout f(int a 10) endl; } void f1(int a) { cout f(int a) endl; }5. 引用5.1. 概念与定义引用是给已存在的变量取一个别名编译器不会为引用变量开辟内存空间 它和它引用的变量共用同一块内存空间。类型 引用别名引用对象;#includeiostream using namespace std; int main() { int a 0; int b a; int c b;//c也相当于是a的引用 c; //abc的值相同 cout a b c endl; //abc的地址相同 cout a endl; cout b endl; cout c endl; return 0; }5.2. 引用的特性引用在定义时必须初始化一个变量可以有多个引用引用不能改变指向一旦引用一个实体就不能再引用其他实体#includeiostream using namespace std; int main() { int a 10; // 编译报错引用 变量 ra 需要初始值设定项 // int ra; int b a; int c 20; // 并非让b引用c引用不能改变指向 // 是一个赋值操作 b c; cout a b c endl; cout a endl; cout b endl; cout c endl; }5.3. 引用的使用引用在实践中主要用于引用传参和引用做返回值使得减少拷贝提高效率和改变引用对象的同时改变被引用对象。引用传参和指针传参的功能是类似的且引用传参相对更加方便。// 函数重载 引用简化程序,使用更加便捷 void Swap(int x, int y) { int tmp x; x y; y tmp; } void Swap(double x, double y) { double tmp x; x y; y tmp; }在部分 C 语言实现的数据结构中可以使用 C 引用代替指针传参在需要使用二级指针等复杂的指针时可以简化程序。#includeiostream using namespace std; typedef int SLTDataType; typedef struct ListNode { SLTDataType val; ListNode* next; }LTNode, *PNode; // 不用二级指针可以简化程序 // void LTPushBack(LTNode** phead, SLTDataType x) // void LTPushBack(LTNode* phead, SLTDataType x) void LTPushBack(PNode phead, SLTDataType x) { PNode newnode (PNode)malloc(sizeof(LTNode)); newnode-val x; newnode-next NULL; if (phead NULL) { phead newnode; } else { //... } } int main() { PNode plist NULL; LTPushBack(plist, 1); return 0; }引用作返回值返回的是返回对象的别名(引用)能够对返回对象进行修改也可以减少拷贝提高效率。传值返回会调用拷贝构造产生一个临时对象右值作为返回值具有常性。因为大部分情况下函数返回的是一个局部变量。#includeiostream #includeassert.h using namespace std; typedef int STDataType; typedef struct Stack { STDataType* arr; int top; int capacity; }ST; void STInit(ST rs, int n 4) { rs.arr (STDataType*)malloc(sizeof(STDataType) * n); rs.top 0; rs.capacity n; } void STPush(ST rs, STDataType x) { if (rs.top rs.capacity) { int newcapacity 2 * rs.capacity; STDataType* tmp (STDataType*)realloc(rs.arr, newcapacity * sizeof(STDataType)); if (tmp NULL) { perror(realloc); exit(1); } rs.arr tmp; rs.capacity newcapacity; } rs.arr[rs.top] x; } // int STTop(ST rs) // 编译报错“” :左操作数必须为左值 // 返回值为拷贝的临时变量是一个右值具有常性 // 返回的是引用是一个左值能够修改返回对象 int STTop(ST rs) { assert(rs.top 0); return rs.arr[rs.top - 1]; } int main() { ST st; STInit(st); STPush(st, 1); STPush(st, 2); STTop(st) 10; cout STTop(st) endl; return 0; }注意不是所有的函数都可以将引用作为返回值。传引用返回是有风险的。如果返回对象是一个当前函数局部域的局部对象函数结束就销毁了那么使用引用返回是有问题的这时的引用相当于一个野引用。野引用会导致很多奇怪的报错且不同的编译器的报错可能不同。传引用返回一定要确保返回对象在当前函数结束后还在才能用引用返回。思考缺省参数函数重载引用以及后面的运算符重载的意义是什么#includeiostream #includeassert.h using namespace std; #define N 100 typedef int SLTDataType; struct SeqList { SLTDataType arr[N]; int size; void Init() { size 0; } void PushBack(int x) { assert(size N); arr[size] x; } // 返回指定位置的值 int AT(int i) { assert(i size); return arr[i]; } int operator[](int i) { assert(i size); return arr[i]; } }; int main() { SeqList s; s.Init(); s.PushBack(1); s.PushBack(2); s.PushBack(3); s.PushBack(4); // 想让偶数位置的值 * 2 // 写法1 for (int i 0; i s.size; i) { if (i % 2 0) { s.AT(i) * 2; } } for (int i 0; i s.size; i) { cout s.AT(i) endl; } // 写法2 for (int i 0; i s.size; i) { if (i % 2 0) { s[i] * 2; } } for (int i 0; i s.size; i) { // cout s.operator[](i) endl; // 编译器转化为下面的函数调用 cout s[i] endl; } return 0; }显然写法2 的可读性更强。通过引用和运算符重载实现了让自定义的数据结构支持类似数组的访问方式。使代码更加直观提高了代码的可读性和易用性让自定义类型的使用体验更加接近内置类型。5.4.const引用引用一个const对象必须用const引用但是const引用可以引用普通对象。这是因为对象的访问权限可以缩小但不能变大。#includeiostream using namespace std; int main() { const int a 10; const int ra a; // 权限不能放大 // 编译报错“初始化” :无法从“const int”转换为“int ” // int ra a; int b 20; // 权限可以缩小 const int rb b; int c a; // 这里是赋值操作 // 不涉及权限的放大和缩小 return 0; }临时对象就是编译器需要一个空间暂存值时临时创建的一个未命名的对象 C 中把这个未命名对象叫做临时对象。函数传值返回表达式运算和类型转换会产生临时对象。C 中规定临时对象具有常性因此使用普通引用将会触发权限放大必须要使用常引用。#includeiostream using namespace std; int main() { int a 10; const int ra 3 * a; //编译报错: “初始化” 无法从“int”转换为“int ” //int ra 3 * a; double b 12.34; const int rb b; //编译报错: “初始化” :无法从“double”转换为“int ” //int rb 3 * b; return 0; }#includeiostream using namespace std; typedef int type; void Test(const int x) { // void Test(int x) // 对不改变参数的引用一般会加上 const // 如果不加上constTest(a)会报错 // 编译报错无法从“const int”转换为“int ” } int main() { const int a 10; int b 20; Test(a); Test(b); return 0; }5.5. 指针和引用C 中指针和引用的功能有重叠性但是各有自己的特点互相不可替代。语法概念上,引用是一个变量的取别名不开辟新的空间而指针是存储一个变量地址需要开辟新的空间。在汇编层面引用是用指针实现的没有引用只有指针。引用和指针都需要开辟新的空间。引用在初始化时引用一个对象后就不能再引用其他对象而指针可以在不断地改变指向对象。sizeof中引用结果为引用类型的大小但指针始终是地址空间所占字节个数。6.inline用inline修饰的函数叫做内联函数编译时 C 编译器会在调用的地方展开内联函数这样调用内联函数就不需要建立栈帧可以提高效率。C语言 中实现宏函数也会在预处理时替换展开但是宏函数实现很复杂很容易出错的且不方便调试。C 设计了inline目的就是替代 C 语言 的宏函数。 在 C 中通常会使用inline代替 C语言 中的宏函数使用const和enum代替 C语言 中的宏常量。实现一个宏函数的常见错误: #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)) 为什么不能加分号? 宏本质是文本替换。 避免宏展开后产生额外语句影响语法结构。 #define ADD(a, b) ((a) (b)); if (1) ADD(1, 2); else cout error; 替换后 if (1) ADD(1, 2); ; // error: else without a previous if else cout error; 为什么要加外面的括号? 保证宏整体作为一个表达式避免运算符优先级问题。 #define ADD(a,b) (a) (b) int x ADD(1,2) * 3; 替换后 int x (1) (2) * 3; 为什么要加里面的括号? 保护传入的参数避免运算符优先级问题。 #define ADD(a,b) (a b) ADD(1 2, 3) 替换后 (1 2 3) - 1 (2 3)inline对于编译器而言只是一个建议也就是说加了inline编译器也可以选择在调用的地方不展开。不同编译器关于inline什么情况展开的情况各不相同因为 C 标准没有规定。一般而言inline适用于频繁调用的短小函数。对于递归函数或代码多些的函数加上inline也会被编译器忽略。inline不建议声明和定义分离到两个文件分离会导致链接错误。因为inline被展开而不是调用因此编译时内联函数不会进入符号表就找不到内联函数的函数地址链接时会出现报错。因次内联函数一般直接定义在.h文件中。vs2022的debug版本下默认是不展开inline的这样方便调试debug版本想展开需要设置一下 以下两个地方。为什么inline只是一个建议因此为了避免程序员滥用内联内联只是给编译器的一个建议。7.nullptrNULL实际是一个宏在stddef.h中可以看到以下代码#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void*)0) #endif #endifC 中NULL可能被定义为字面常量 0或者在 C 中被定义为无类型指针(void*)的常量。不论采取何种定义在使用空值的指针时不可避免的会遇到一些麻烦。C11 中引入nullptrnullptr是⼀个特殊的关键字nullptr是⼀种特殊类型的字面量它可以转换成任意其他类型的指针类型。使用nullptr定义空指针可以避免类型转换的问题因为nullptr只能被隐式地转换为指针类型而不能被转换为整数类型。#includeiostream using namespace std; 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(NULL)调⽤指针版本的f(int*)函数 // 但是由于NULL被定义成0调⽤了f(intx)因此与程序的初衷相悖。 f((int*)NULL); // 需要强制类型转换才能调用函数 // f((void*)NULL); // 编译报错: “f”: 2个重载中没有⼀个可以转换所有参数类型 f(nullptr); return 0; }