cpp-intro-basics C 入门基础从命名空间到引用、inline 与 nullptrC 对很多 C 语言学习者来说并不陌生因为它兼容 C 的大部分语法。但真正开始写 C 后会发现它并不是“C 加几个新关键字”这么简单。它在 C 的基础上引入了命名空间、函数重载、引用、默认参数、内联函数、面向对象、泛型和标准库等能力让大型程序更容易组织也让代码表达能力更强。这篇文章面向已经有一点 C 基础、准备正式进入 C 的读者。我们不追求一次讲完所有语法而是抓住入门阶段最容易遇到、也最容易混淆的几个点namespace、C 输入输出、缺省参数、函数重载、引用、const引用、inline和nullptr。一、为什么要学 CC 的起源可以追溯到 C 语言之后的系统级开发需求。C 语言高效、接近底层但在大型软件开发中单纯依靠全局函数、结构体和手动约定来组织代码维护成本会逐渐变高。C 在保留 C 高性能和底层能力的同时增强了抽象能力。常见应用方向包括方向典型场景系统软件编译器、数据库、浏览器、操作系统组件服务端高性能网络服务、游戏服务、流媒体服务游戏开发游戏引擎、客户端核心模块音视频处理FFmpeg、WebRTC 等相关方向嵌入式车载系统、智能硬件、设备控制基础库和工具算法库、框架、底层工具链机器学习系统底层算子、推理引擎、性能敏感模块C 难点不只在语法数量多更在于它同时服务于“接近底层”和“高级抽象”两类需求。入门时不要急着追求所有特性先把基础语法的设计动机和使用边界弄清楚会比死记语法更稳。二、第一个 C 程序C 语言版本的 hello world 在 C 中依然可以运行#includestdio.hintmain(){printf(hello world\n);return0;}但更符合 C 风格的写法是使用标准输入输出流#includeiostreamintmain(){std::couthello worldstd::endl;return0;}编译运行g hello.cpp-stdc11-ohello ./hello预期输出hello world这里有几个符号第一次看到会有点陌生std是标准库所在的命名空间cout是标准输出流对象在这里是流插入运算符不是位运算左移endl表示输出换行并刷新缓冲区。很多入门代码会写using namespace std;这样就可以直接写cout。不过在真实工程中更推荐写std::cout、std::endl避免把整个标准库命名空间展开后造成名字冲突。三、namespace解决命名冲突C/C 程序中会出现大量变量、函数、类型名。如果都放在全局作用域很容易冲突。比如自己定义了一个rand就可能和库函数rand撞名。命名空间的作用是把一组名字放进一个独立的作用域降低命名冲突风险。#includeiostreamnamespaceproject_a{intvalue10;intAdd(intleft,intright){returnleftright;}}namespaceproject_b{intvalue20;intAdd(intleft,intright){return(leftright)*10;}}intmain(){std::coutproject_a::valuestd::endl;std::coutproject_b::valuestd::endl;std::coutproject_a::Add(1,2)std::endl;std::coutproject_b::Add(1,2)std::endl;return0;}命名空间有几个常见特点namespace只能定义在全局作用域命名空间可以嵌套多个文件中同名命名空间会被视为同一个命名空间C 标准库放在std命名空间中命名空间影响名字查找不直接改变变量生命周期。使用命名空间常见方式有三种// 方式一指定命名空间访问工程中推荐std::couthellostd::endl;// 方式二只展开某个成员usingstd::cout;usingstd::endl;couthelloendl;// 方式三展开整个命名空间小练习可以用工程中慎用usingnamespacestd;很多初学者会觉得std::麻烦但它让名字归属非常清楚。项目一大清楚比省几个字符更重要。四、C 输入输出流C 使用iostream提供标准输入输出对象或符号含义std::cin标准输入流std::cout标准输出流std::endl换行并刷新缓冲区流插入用于输出流提取用于输入一个简单例子#includeiostreamintmain(){inta0;doubleb0.0;charcx;std::cinabc;std::couta b cstd::endl;return0;}和scanf/printf相比C 流不需要手动写%d、%lf这类格式控制符。它可以根据变量类型选择合适的输入输出逻辑这背后和函数重载、运算符重载有关。如果遇到大量输入输出场景可以加上下面几行提升 C 流的效率std::ios_base::sync_with_stdio(false);std::cin.tie(nullptr);std::cout.tie(nullptr);需要注意一旦关闭同步就不要在同一段程序里随意混用 C 风格 IO 和 C 风格 IO否则可能出现输出顺序不符合预期的问题。五、缺省参数给函数参数设置默认值缺省参数也常叫默认参数。它允许我们在声明或定义函数时给参数指定默认值调用时如果没有传实参就使用默认值。#includeiostreamvoidPrint(intvalue0){std::coutvaluestd::endl;}intmain(){Print();// 使用默认值 0Print(10);// 使用传入值 10return0;}缺省参数分两类// 全缺省所有参数都有默认值voidFunc1(inta10,intb20,intc30);// 半缺省部分参数有默认值voidFunc2(inta,intb10,intc20);这里有两条规则必须记住半缺省参数必须从右往左连续给默认值函数调用传参必须从左往右依次传不能跳过中间参数。下面这种写法是错误的// 错误默认参数不能中间断开voidFunc(inta10,intb,intc30);声明和定义分离时默认值通常放在声明处定义处不要重复写// Stack.hvoidInit(intcapacity4);// Stack.cppvoidInit(intcapacity){// ...}如果声明和定义都写默认值编译器会报重复指定默认参数的错误。六、函数重载同名函数也可以共存C 支持在同一作用域中定义多个同名函数只要它们的参数列表不同。参数列表不同可以体现在参数类型不同参数个数不同参数类型顺序不同。#includeiostreamintAdd(intleft,intright){std::coutint Addstd::endl;returnleftright;}doubleAdd(doubleleft,doubleright){std::coutdouble Addstd::endl;returnleftright;}voidPrint(){std::coutPrint()std::endl;}voidPrint(intvalue){std::coutPrint(int)std::endl;}intmain(){Add(1,2);Add(1.1,2.2);Print();Print(10);return0;}返回值不同不能构成重载// 错误仅返回值不同无法重载// int Func();// double Func();原因很简单调用Func()时编译器无法仅根据返回值判断你想调用哪个版本。缺省参数和重载放在一起时也容易出问题voidf();voidf(intvalue10);// f(); // 存在歧义编译器不知道调用哪个所以默认参数不要滥用尤其不要和重载组合出让人难以判断的调用形式。七、引用给已有对象取别名引用不是新定义一个对象而是给已有对象起别名。#includeiostreamintmain(){inta10;intraa;intrbra;rb5;std::coutastd::endl;// 15std::coutastd::endl;std::coutrastd::endl;std::coutrbstd::endl;return0;}引用有三个重要特性定义时必须初始化一个对象可以有多个引用引用一旦绑定到某个对象就不能再改为引用其他对象。下面这点特别容易误解inta10;intb20;intra;rb;// 不是让 r 改为引用 b而是把 b 的值赋给 a引用最常见的用途是传参和返回值。引用传参引用传参可以避免拷贝也可以让函数内部修改外部对象。voidSwap(intx,inty){inttmpx;xy;ytmp;}和指针传参相比引用写起来更自然inta1;intb2;Swap(a,b);引用返回引用返回可以让函数调用结果作为左值使用但要非常注意生命周期。intTop(int*array,intsize){returnarray[size-1];}intmain(){intarr[3]{1,2,3};Top(arr,3)10;std::coutarr[2]std::endl;// 13return0;}补充说明返回引用时必须保证返回对象在函数结束后仍然存在。不要返回局部变量的引用因为局部变量离开函数后生命周期结束。八、const 引用权限只能缩小不能放大普通引用不能绑定到const对象constinta10;// int ra a; // 错误试图通过普通引用修改 const 对象constintraa;const引用可以绑定普通对象因为这相当于缩小访问权限intb20;constintrbb;// rb; // 错误不能通过 const 引用修改对象const引用还能绑定临时对象inta10;// int r1 a * 3; // 错误constintr2a*3;doubled12.34;// int r3 d; // 错误类型转换会产生临时对象constintr4d;这里的关键是表达式结果、类型转换结果经常会产生临时对象临时对象具有常性不能用普通引用绑定只能用const引用绑定。九、指针和引用的区别引用和指针有重叠场景但不是同一个东西。对比点引用指针本质已有对象的别名保存对象地址的变量初始化定义时必须初始化语法上不是必须但建议初始化是否能改变指向不能可以访问方式直接访问需要解引用空值问题一般不表示空可能为空指针sizeof引用类型大小指针本身大小常见 64 位下 8 字节简单理解如果参数一定有效并且只是想简化调用优先考虑引用如果参数可能为空或者需要改变指向指针更合适引用不是为了完全替代指针而是让常见场景写得更安全、更自然。十、inline替代宏函数的一种更安全方式用inline修饰的函数称为内联函数。它的设计目标是减少短小函数调用时建立栈帧的成本。inlineintAdd(intx,inty){returnxy;}inline更适合短小、频繁调用的函数。它只是给编译器一个建议最终是否展开由编译器决定。递归函数、代码较长的函数即使写了inline也可能不会被展开。C 语言里常用宏函数减少调用成本#defineADD(a,b)((a)(b))宏函数有不少坑比如括号少了会导致优先级错误也不方便调试。inline保留函数的类型检查和调试友好性因此比宏函数更适合大多数简单函数封装。还有一个细节inline函数不建议只声明在头文件、定义放到单独源文件中。因为内联展开需要在调用处看到函数定义否则可能引发链接问题。常见做法是把inline函数定义直接放在头文件里。十一、nullptr更安全的空指针传统 C/C 代码中经常使用NULL表示空指针。但在 C 中NULL可能被定义为字面量0这会在函数重载场景下带来问题。#includeiostream#includecstddefvoidf(intx){std::coutf(int)std::endl;}voidf(int*ptr){std::coutf(int*)std::endl;}intmain(){f(0);f(NULL);// 容易出现非预期匹配或歧义不推荐f((int*)NULL);// 明确调用指针版本f(nullptr);// C11 后推荐return0;}常见实现中NULL可能被当作0从而匹配到整数版本。nullptr是 C11 引入的关键字它可以转换为任意指针类型但不会被当作整数使用。因此表达空指针时推荐使用nullptr。十二、常见问题与易错点1. 为什么工程中不建议直接using namespace std因为它会把整个std命名空间展开项目变大后容易造成命名冲突。小练习可以图方便工程代码更建议写std::cout或只展开少量常用成员。2. 缺省参数为什么必须从右往左给函数调用传参是从左往右匹配的。如果中间参数缺省后面的实参就无法判断应该匹配哪个形参因此 C 要求默认值从右侧连续给出。3. 函数重载为什么不能只看返回值调用函数时编译器主要根据函数名和实参列表匹配目标函数。仅返回值不同调用表达式本身无法区分要调用哪一个。4. 引用是不是一定比指针好不是。引用适合“对象一定存在”的场景指针适合“可能为空”或“需要改变指向”的场景。二者是互补关系。5.const int r a * 3为什么可以a * 3的结果是临时对象临时对象不能被普通引用绑定但可以被const引用绑定。const引用会延长该临时对象的生命周期到引用的生命周期。6.inline一定会展开吗不一定。inline是建议不是命令。编译器会根据函数复杂度、优化级别、调试模式等因素决定是否展开。十三、总结C 入门阶段不要把所有语法点孤立记忆可以按“解决什么问题”来理解namespace解决命名冲突iostream提供类型更友好的输入输出方式缺省参数让函数调用更灵活函数重载让同一操作可以适配不同参数引用让传参和返回更自然但要关注生命周期const引用解决权限控制和临时对象绑定问题inline用更安全的方式替代宏函数的部分场景nullptr比NULL更适合表达空指针。这些内容看起来分散本质上都在增强 C 的表达能力和工程组织能力。把这些基础打稳再继续学习类和对象、STL、模板、智能指针等内容时很多语法设计就会顺起来。