目录绪论C版本更新一、C的第一个程序二、命名空间2.1 namespace2.2 作用域操作符 ::2.3 命名空间的使用三、C输入输出四、缺省参数五、函数重载六、引用6.1 定义6.2 特性6.3 使用6.4 const引用6.5 指针和引用的关系七、inline函数八、nullptr此处省略C的发展历史……绪论C版本更新C从多年一更变成了固定的三年一更中间经历了许多的插曲最后思路转变成从 等所有东西都完美了再发布 变成 到点就发好的坏的都按班车走。这就是现在能看C11/14/17/20/23/26 这样整齐的 3 年节奏。C参考文档1.https://legacy.cplusplus.com/reference/— 非官方只到 C11但按头文件组织、好懂适合入门查用法2.https://zh.cppreference.com/w/cpp— 官方中文版信息全、更新到最新标准但偏硬核3.https://en.cppreference.com/w/— 官方英文版最权威内容同中文版总结入门查用法看第一个查最新标准细节看后两个结合着用。一、C的第一个程序C兼容C语言绝大多数的语法所以C语言实现的Hello World依旧可以运行C需要把定义的文件后缀改为 .cpp vs编译器看到 .cpp 就会调用C编译器编译。其他平台也有相关的C编译器比如Linux下是g。// C代码 #includeiostream using namespace std; int main() { cout Hello World! endl; return 0; }二、命名空间2.1 namespace本质是圈出一个独立作用域解决同名冲突。语法namespace 名字 { 变量/函数/类型 }规则1、C有四种域函数局部域、全局域、命名空间域、类域都用于编译时名字查找。2、局部域/全局域会影响变量声明周期命名空间域/类域不影响的生命周期。3、namespace只能写在全局可以嵌套多文件里同名namespace自动合并不冲突。4、C标准库全部放在std命名空间里。总结namespace 就是给变量 / 函数套个 家族姓避免不同模块撞名只管编译时叫谁不管变量活多久。以下C语言代码会报错#includestdio.h #includestdlib.h int main() { int rand 9; printf(%d, rand); return 0; }报错如下“找到一个或多个重定义的符号”主要原因是stdlib标准库中已经有一个名为 rand 的函数此时在C语言这里再次定义一个整型变量命名为rand就会报错需要C中namespace来解决C解决代码如下#includestdio.h #includestdlib.h namespace xys { int rand 9; // 函数 int add(int a, int b) { return a b; } // 类型 struct Stu { char name; int age; }; } int main() { printf(%d, xys::rand); return 0; }利用namespace来声明了一个域把整型变量rand放在改域里就不会报错了同时也可以在域中可以定义变量、函数、类型。细心的博友们已经在这里发现了我在问namespace中的rand时加上了前缀 xys:: 那么接下来我来详细讲解为什么加上此前缀以及新的操作符。2.2 作用域操作符 ::作用明确指定标识符所属的作用域解决命名冲突、访问被内层作用域屏蔽的外层实体。1. 访问全局作用域当局部变量与全局变量命名冲突时局部变量会屏蔽全局变量在变量名前加 :: 即课显示访问全局作用域的实体。int num 100; // 全局变量 int main() { int num 10; // 局部变量屏蔽了全局num cout num; // 输出 10局部变量 cout ::num; // 输出 100全局变量 }2. 访问命名空间成员std::cout hello; // 访问 std 命名空间中的 cout std::string str; // 访问 std 命名空间中的 string嵌套命名空间可逐层使用 :: 访问namespace A { namespace B { int val 5; } } int x A::B::val; // 访问嵌套命名空间的成员3. 访问类的成员在类外部定义成员函数时必须用类名::函数名指明该函数的所属类。访问类的静态成员静态变量/静态函数时直接通过类名 ::访问无需实例化对象。class Student { public: static int count; // 静态成员声明 void show(); // 成员函数声明 }; // 类外定义成员函数 void Student::show() { /* ... */ } // 类外初始化静态成员 int Student::count 0; int main() { int n Student::count; // 直接通过类名访问静态成员 }2.3 命名空间的使用编译器默认仅在局部和全局作用域查找标识符不会自动检索命名空间直接访问命名空间内的成员会编译报错。使用命名空间成员有三种方式1、指定命名空间访问加::前缀项目开发推荐使用namespace N { int apple 3; int banana 5; } int main{ std::cout N::apple std::endl; return 0; }通过命名空间名 :: 成员名明确指定作用域无命名冲突风险我们常用的std::out就属于这种方式。2、using 展开单个成员适合频繁访问且无命名冲突的成员namespace N { int apple 3; int banana 5; } using N::apple; // 只展开命名空间N中的成员apple int main{ std::cout apple std::endl; // apple 前不用再加上 N:: return 0; }仅展开指定的成员该成员可直接使用未展开的成员仍需加命名空间前缀适合频繁访问且无冲突的成员。3、展开全部命名空间成员仅适合小型练习代码项目中冲突风险高不推荐namespace N { int apple 3; int banana 5; } using namespace N; // 展开N命名空间的所有成员 int main{ std::cout apple std::endl; // apple 前不用加上 N:: std::cout banana std::endl; // banana 前不用加上 N:: return 0; }using namespace 命名空间名会把该命名空间所有成员全部引入当前作用域写法简便但易引发命名冲突大型项目不推荐。我们日常练习代码里常见using namespace std;就是第三种用法可以省略std::前缀直接写cout、string但正式项目中一般不这么写。三、C输入输出1、iostream是标准输入输出流库提供std::cin标准输入流对象、std::out标准输出流对象std::endl是操纵符作用为换行并刷新输出缓冲区。2、通过流插入 / 输出、流提取 / 输入运算符完成 IO 操作依靠函数重载自动识别变量类型无需手动指定格式同时支持自定义类型的输入输出。3、所有 IO 组件都归属于std命名空间练习代码可用using namespace std;简化写法正式项目不推荐。4、VS 编译器下引入iostream可间接使用printf/scanf但不具备通用性大数据量输入场景如算法竞赛可通过关闭 stdio 同步、解绑 cin 与 cout 来提升 IO 效率。#include iostream using namespace std; // 展开std命名空间仅推荐练习使用 int main() { double point 0.0; cin point; cout point endl; int juice 0; cin juice; cout juice endl; return 0; }四、缺省参数定义函数声明时给参数一个默认值调用时不传就自动用这个默认值传进去。代码示例#include iostream // b和c都有默认值 int sum(int a, int b 0, int c 0) { return a b c; } using namespace std; int main() { cout sum(1); // 1相当于sum(1, 0, 0) cout sum(1, 2); // 3相当于sum(1, 2, 0) cout sum(1, 2, 3); // 6全自己传 }全缺省所有参数都有默认值半缺省只有部分参数有默认值三条硬规则1.半缺省必须从右往左连续给默认值不能跳着给void f(int a, int b 10, int c); // 错误b有默认值但c没有c在b右边 void f(int a, int b, int c 10); // 正确 void f(int a, int b 10, int c 20); // 正确2.声明和定义分离时默认值只写在声明里// 头文件里 void f(int a 10); // .cpp里 void f(int a) { ... } // 不要再写10否则重复定义3.不能和函数重载产生歧义void f(int a 10) { } void f() { } // 错误调用f()时编译器不知道调哪个 // 是调无参版本还是调f(10)底层原理缺省参数不是运行时填充的是编译期在调用点直接把默认值塞进去sum(1); // 编译后等价于 sum(1, 0, 0);五、函数重载定义同一个作用域可以定义同名函数但参数列表必须不同C语言不支持C支持三种合法重载形式1、参数类型不同Add(int,int); Add(double,double);2、参数个数不同Add(int,int); Add(int,int,int);3、参数类型顺序不同f(int,char); f(char,int);不能算重载的情况1、返回值不同调用时编译器只看实参看不到返回值无法区分。2、和缺省参数产生歧义f() 和 f(int a 10) 同时存在时调 f() 编译器不知道该选哪个产生歧义。六、引用6.1 定义本质不是新定义变量而是给已存在的变量取个别名编译器不为引用分配新内存它和原变量共用同一块内存空间。语法类型 别名 原变量;代码示例#includeiostream using namespace std; int main() { int a 10; int b a; int c b; int d c; cout a endl; cout b endl; cout c endl; cout d endl; cout a endl; cout b endl; cout c endl; cout d endl; return 0; }运行结果6.2 特性三个规则1、引用在定义时必须初始化。2、一个变量可以有多个引用。3、引用一旦绑定就不能改绑。6.3 使用引用的使用主要在两部分引用传参和引用返回引用传参类似指针传参功能相同但时写法更简洁——函数改形参就是改实参还省去了拷贝。代码示例void Swap(int rx, int ry) { int tmp rx; rx ry; ry tmp; } int main() { int x 0, y 9; Swap(x, y); // 直接传变量不用传地址 // x变成1y变成0 cout x y endl; Swap(x, y); cout x y endl; }传引用返回代替传值返回避免产生临时对象还能让返回值当左值使用。代码示例// 传值返回返回临时对象不能做左值 // int STTop(ST rs) { // return rs.a[rs.top]; // } // STTop(st1) 10; // 编译错误左边必须是左值 // 传引用返回返回栈顶元素的别名 int STTop(ST rs) { assert(rs.top 0); return rs.a[rs.top]; } int main() { ST st1; STInit(st1); STPush(st1, 1); STPush(st1, 2); STTop(st1) 10; // 直接把栈顶元素2改成12 cout STTop(st1); // 输出12 }小补充左值 vs 右值一个简单的判断方法一个表达式能不能取地址能取地址 →左值不能取地址 →右值左值lvalue)右值rvalue)本质有名字、有持久内存的东西临时的、用完即毁的东西能不能取地址能x合法不能5编译错误能不能放左边能不能能不能放右边能能生命周期一直存在直到离开作用域语句结束就销毁代替二级指针——链表头插typedef struct ListNode { int val; struct ListNode* next; } LTNode, *PNode; // 用 PNode phead 代替 LTNode** phead写法更直观 void ListPushBack(PNode phead, int x) { PNode newnode (PNode)malloc(sizeof(LTNode)); newnode-val x; newnode-next NULL; if (phead NULL) { phead newnode; // 直接改phead不用*phead newnode } } int main() { PNode plist NULL; ListPushBack(plist, 1); // 直接传plist不用plist }总结引用传参就是给实参起个别名进函数用传引用返回就是给函数内部某个变量起别名带出去——少写指针、少拷贝、还能直接改。6.4 const引用const引用规则权限只能缩小不能放大。例如普通变量读写权限→ const 引用只读权限可以缩小权限const 变量只读权限→ 普通引用想读写不可以放大权限临时对象必须用 const 引用绑定像 a*3 的结果、double 转 int 产生的中间值编译器会生成一个临时对象暂存这个临时对象自带常性普通引用绑不上必须用const int main() { // 1. const对象必须用const引用 const int a 10; // int ra a; // ✗ 权限放大a只读你想通过ra改它 const int ra a; // ✓ 正确 // 2. 普通对象可以用const引用权限缩小 int b 20; const int rb b; // ✓ rb只读但b本身还能改 // rb; // ✗ rb是const不能通过rb改 // 3. 临时对象必须用const引用 // int rc a * 3; // ✗ a*3是临时对象常性普通引用绑不上 const int rc a * 3; // ✓ // 4. 类型转换也产生临时对象 double d 12.34; // int rd d; // ✗ double转int产生临时对象 const int rd d; // ✓ }6.5 指针和引用的关系对比项引用指针本质变量别名不开新内存存储地址的变量要开空间初始化定义时必须初始化建议初始化语法上不强制改指向绑死一个对象后不能再换可以随意改指向访问方式直接用不用解引用要 * 解引用才访问对象sizeof结果是引用类型本身的大小永远是地址大小32 位 4 字节64 位 8 字节安全性很少出问题相对安全容易出现空指针、野指针总结引用是更安全、更简洁的 别名但不能改绑、不能空指针更灵活但要自己管地址、容易出错。实践中该用哪个用哪个不能互相替代。七、inline函数为什么要有inline函数C 语言的宏函数在预处理时直接文本替换复杂宏容易出错、不好调试C 设计 inline 就是为了替代宏函数解决宏的问题。inline函数能做什么建议编译器在调用点把函数体直接展开省去函数调用的压栈 / 跳转 / 返回开销和宏效果类似但它是真正的函数有类型检查、作用域注意1、inline 只是给编译器的建议不是强制函数太复杂递归、循环太多编译器不会展开。2、vs 调试debug 版本默认不展开 inline方便打断点调试release 版本才展开想在 debug 里展开要改项目设置。3、关键规则inline 的声明和定义必须放同一个文件一般直接写在头文件里不能分离到.h 和.cpp否则链接错误。#include iostream using namespace std; // 直接写在头文件/同一个文件里 inline int Add(int x, int y) { return x y; } int main() { int ret Add(8, 9) * 3; cout ret endl; }对比宏函数的坑// 宏的问题不加括号会出bug // #define ADD(a, b) a b // 错误ADD(1,2)*5 会变成 12*511 #define ADD(a, b) ((a) (b)) // 正确外层括号内层括号都要加 int main() { cout ADD(1, 2) * 5 endl; // ((1)(2))*5 15 }错误用法声明定义分离导致链接错误// 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); // 链接错误找不到f的地址inline被展开后没有独立函数地址 }总结inline 就是 带类型检查的宏—— 写在头文件里建议编译器把函数体直接塞进调用点既快又安全千万别把声明和定义分到两个.cpp 文件里否则链接报错。八、nullptr定义nullptr是C11引入的关键字专门用来表示空指针。它不是0不是NULL是一个独立类型 nullptr_t 的字面值。// 头文件里定义不用自己写 typedef decltype(nullptr) nullptr_t;注意nullptr只能隐式转换成指针类型不能转成整数。什么是隐式转换隐式转换 编译器自动帮你转你不用写任何代码。显式转换 你自己写强转比如 (int*)NULL 。问题在C中NULL就是 0void f(int x) { cout f(int); } void f(int* ptr) { cout f(int*); } f(0); // 调 f(int) f(NULL); // 还是调 f(int)因为NULL在C里被定义成0不是指针 f((int*)NULL); // 手动强转才能调到指针版本 f(nullptr); // ✓ 明确调用 f(int*)不会有歧义总结以前用 NULL 表示空指针但 NULL 在 C 里就是 0函数重载时会误调到 int 版本C11 用 nullptr 代替它专门表示空指针不会和整数 0 混淆。最后感谢你的阅读欢迎交流与指正下一篇我们再见Bye bye
阅读完成 · 觉得有帮助?