资讯详情

资讯详情

C++入门不只Hello World:内存、类型与表达式求值底层机制

简介《C入门、核心与提高讲义笔记》以一份PDF文档承载面向刚接触C的编程学习者以及需要系统梳理语法与进阶概念的开发者帮助其在较短时间内建立从基础语法到面向对象、泛型编程的知识框架。文件共1个为PDF格式压缩包约5.07MB便于在电脑或平板上直接阅读与检索。内容围绕第一个C程序、注释、变量与常量、关键字、标识符命名规则、整型与浮点型等数据类型展开并进一步延伸到指针、数组、结构体、类、运算符重载、模板和异常处理等提高主题知识点之间衔接较紧适合作为课程配套笔记或自学提纲。已有25997人学习说明其在C入门资料中具有一定参考价值。读者可借助这份讲义快速定位语法要点、对照示例理解概念并在复习时按章节查漏补缺为后续项目实践和算法学习打基础。1. 第一个程序不只是打印字符串C 入门讲义为什么要从内存讲起很多人拿到一份 C 入门讲义笔记第一件事是照着敲 Hello world看到控制台打出那行字就翻页了。跑通只能说明工具链没坏真正的分水岭在跑通之后——#include iostream这一行把「语言本身」和「标准库」切成了两半main的返回值和参数决定了程序如何被操作系统调用cout 背后是一条类型安全的运算符重载链。这三件事没有一件是语法糖它们都是 C 这门编程语言在设计上主动做的取舍也是后面讲变量、常量、数据类型、运算符时反复要回到的原点。一份讲义笔记真正的价值不在于把语法列全而在于每一节都能回答「编译器在这里替你做了什么」入门部分讲语法长什么样核心部分讲机制为什么这样转提高部分讲代价花在了哪里。这一篇就按这个顺序往下拆从字节和类型开始路过符号和表达式最后落到抽象的成本上。2. 类型与字节为什么int是 4 字节只是平台约定C 标准里从来没有规定过int必须是 4 字节。标准只保证short至少 16 位、int至少和short一样宽、long至少 32 位、long long至少 64 位并且满足sizeof(char) 1、sizeof(char) sizeof(short) sizeof(int) sizeof(long) sizeof(long long)这条单调链。换句话说「int占 4 字节」是 Windows 的 LLP64 和 Linux 的 LP64 两大 ABI 各自拍板的实现约定不是语言的法律。这个区别不是学究式的抠字眼。同一份源码拿到不同平台上sizeof(long)在 64 位 Windows 上是 4在 64 位 Linux 上是 8结构体里塞一个long整体布局和跨进程通信的二进制协议就跟着变。入门讲义里那张「数据类型占用空间」的表本质上是给某个具体平台拍的快照学的时候要记住它的前提而不是把数字当成真理背下来。2.1sizeof为什么必须是个编译期运算符sizeof长得像函数其实是运算符优先级和一元运算符同级结果类型是size_t。它的值在绝大多数情况下于编译期就确定了原因很硬编译器要给局部变量分配栈帧栈帧大小必须先知道数组的元素步长、指针算术的偏移量、结构体的字段偏移全都依赖类型的大小。如果sizeof要等到运行时才能算出来这些事一件都做不了。#include iostream using namespace std; struct Packed { char c; // 1 字节 int i; // 4 字节但对齐到 4 的倍数 char d; // 1 字节 }; int main() { int arr[10] {0}; // 对类型求 sizeof编译期常量 cout sizeof(int) sizeof(int) endl; cout sizeof(long) sizeof(long) endl; cout sizeof(long long) sizeof(long long) endl; // 对数组求 sizeof整个数组的字节数不是元素个数 cout sizeof(arr) sizeof(arr) endl; // 40 cout 元素个数 sizeof(arr) / sizeof(arr[0]) endl; // 10 // 对结构体求 sizeof包含编译器插入的填充字节 cout sizeof(Packed) sizeof(Packed) endl; // 通常是 12 而不是 6 return 0; }这段代码里有三个容易被略过的细节。第一sizeof(arr)得到的是整个数组的字节数 40因为arr的类型是int[10]sizeof尊重静态类型一旦把arr传给函数它会退化成int*再求sizeof就只有 864 位平台这是入门阶段最常见的翻车点之一。第二sizeof(Packed)不是 1416编译器为了满足int字段的四字节对齐会在c后面补 3 个字节、在d后面再补 3 个字节凑成 12。填充字节不属于任何字段内容是不确定的所以拿memcmp比较两个结构体对象会得到玄学结果。第三sizeof的操作数是类型名时要加括号是表达式时可以不加sizeof arr和sizeof(arr)等价但sizeof(int)不能省括号——sizeof int是语法错误。sizeof的一个例外是 C99 引入的可变长数组C 标准里没有它但部分编译器作为扩展支持。一旦数组长度是运行期变量sizeof就必须退化成运行期计算。这也是为什么标准库容器std::vector没有sizeof语义上的「元素个数」——它的大小是固定的对象头元素个数得问.size()。2.2 浮点数的「7 位有效数字」是怎么算出来的讲义里写float有 7 位有效数字、double有 15 到 16 位这个数字不是拍脑袋定的是 IEEE 754 的位宽除以 log₂10 得来的。float占 32 位布局是 1 位符号 8 位指数 23 位尾数。尾数那 23 位再加上隐含的最高位 1一共 24 位二进制精度。二进制位数换算成十进制位数就是24 × log₁₀2 ≈ 24 × 0.301 ≈ 7.22取整得 7。double是 1 11 5252 1 53 位精度53 × 0.301 ≈ 15.95所以是 15 到 16 位。#include iostream #include iomanip using namespace std; int main() { float f 0.1f; double d 0.1; // 默认输出 6 位有效数字看起来一样其实底层位模式不同 cout f f endl; cout d d endl; // 摊开看setprecision 控制的是有效数字位数 cout setprecision(20); cout f f endl; // 0.10000000149011611938 cout d d endl; // 0.10000000000000000555 // 有效数字丢失的经典演示 float sum 0.0f; for (int i 0; i 10; i) sum 0.1f; cout 10 次累加 0.1f sum endl; // 1.000000119... cout 与 1.0f 相等吗 (sum 1.0f) endl; // 0 return 0; }代码里0.1在二进制下是无限循环小数和十进制里的 1/3 一样写不完只能截断到 23 位或 52 位尾数。截断产生的误差在单次运算里看不出来累加十次就浮出水面了。这就是为什么浮点数比较永远不要用而要判断两个数的差是否小于一个 epsilon。还有一处容易误解cout默认输出 6 位有效数字float和double打印出来长得一样很多人就以为两者没差别。setprecision(20)一摊开float从第 8 位开始就是垃圾数据了。反过来float f 3.14;这种写法里3.14字面量本身是double赋给float会发生一次窄化转换编译器可能给警告正确写法是3.14f加f后缀让字面量本身就以单精度构造。2.3char与bool一个字节里的两种设计哲学char占 1 字节但存的不是「字符」是字符对应的整数编码。a在内存里是 970是 48 而不是 0这个 48 的偏移是新手最常栽的跟头——0 1得到的是1而0 1得到的是整数 1两者完全不是一回事。#include iostream using namespace std; int main() { char ch a; cout ch endl; // a按字符解释 cout (int)ch endl; // 97强制按整数解释同一块内存 cout sizeof(ch) endl; // 1 ch 97; // 直接用编码赋值等价于 ch a cout ch endl; // a // \n 只是一个整数 10 的另一种写法 cout (int)\n endl; // 10 bool flag true; cout flag endl; // 1不是 true cout sizeof(flag) endl; // 1不是 1 bit flag false; cout flag endl; // 0 // bool 的「非零即真」任何非 0 值赋给 bool 都变成 true bool weird 42; cout weird endl; // 1 return 0; }bool只占 1 字节而不是 1 位是因为现代 CPU 的最小可寻址单位就是字节没有办法单独寻址某一位。想省空间只能自己用位运算打包代价是每次读写都要做移位和掩码。标准只规定bool至少能表示 true 和 false没规定具体字节数但主流实现都取 1。char的符号性是个历史遗留问题char到底是有符号还是无符号由实现决定。ASCII 范围内的值不受影响但一旦拿char存大于 127 的值或者用它做数组下标就可能踩到负数。需要确定语义时标准提供了signed char和unsigned char两个明确类型char则保持「用来存字符」的模糊定位。3. 常量与符号#define和const之间隔着一整条编译流水线#define day 7和const int day 7;在源码层面看起来只差几个字符但它们在编译器内部走的是完全不同的两条路。前者的处理发生在编译器真正开始工作之前后者的处理发生在类型系统内部。这个差别决定了很多实际行为调试器能不能看到这个符号、编译器能不能做类型检查、这个常量有没有作用域、能不能取地址。3.1 宏替换发生在语法分析之前所以它没有类型C 的编译流程大致是预处理 → 词法分析 → 语法分析 → 语义分析 → 代码生成。#define在第一步就被处理掉了处理方式极其粗暴——纯文本替换把源码里所有出现day的地方换成7然后预处理器把结果交给编译器编译器根本不知道曾经有过一个叫day的东西。#include iostream using namespace std; #define DAY 7 // 预处理阶段就被抹掉 #define SQUARE(x) x * x // 经典陷阱没有括号保护 int main() { cout DAY endl; // 展开后是 2 3 * 2 3等于 11不是 25 cout SQUARE(2 3) endl; const int month 12; // 真正的常量有类型、有作用域 // 调试试试在调试器里能看见 month看不见 DAY int arr[month]; // 合法month 是编译期常量表达式 return 0; }SQUARE(2 3)展开成2 3 * 2 3因为宏只做文本替换不做任何优先级处理。这是宏最著名的一个坑正确写法是#define SQUARE(x) ((x) * (x))给参数和整体都套上括号。但即便加了括号SQUARE(i)这类写法依然会在展开后把i自增两次宏没有办法解决这个问题——它不理解「表达式」这个概念。const定义的是一个真正的变量只是被标记为不可修改。它有类型所以编译器能做类型检查它有作用域所以可以定义在函数内部、类内部、命名空间里它在符号表里有名字调试器能看见只要不是extern且被 odr-use编译器通常直接把它折叠成立即数不占运行时存储。这就是 C 里推荐用const以及 C11 之后的constexpr替代宏常量的根本原因。还有一层#define是全局的。一旦在头文件里写了#define DAY 7所有包含这个头文件的翻译单元都会被迫接受这个名字之后任何叫DAY的变量、函数、成员都会炸。const遵守作用域规则可以关在命名空间或类里不会污染别人。这就是为什么现代 C 代码里几乎看不到宏常量只剩条件编译和头文件保护还在用宏。3.2 关键字列表是编译器词法分析的保留字表讲义里那一长串asm do if return typedef auto double inline short typeid bool dynamic_cast int signed typename break else long sizeof union case enum mutable static unsigned catch explicit namespace static_cast using char export new struct virtual class extern operator switch void const false private template volatile const_cast float protected this wchar_t continue for public throw while default friend register true delete goto reinterpret_cast try本质上是编译器词法分析器的保留字表。编译器读源码的第一步是词法分析把字符流切成一个个 token。切的时候它维护一张表遇到int就把这个 token 标记成「整型关键字」而不是「标识符」。这个分类在词法阶段就完成了后面语法分析器拿到 token 序列看到关键字 token 就按对应语法规则处理看到标识符 token 就按变量、函数名处理。两者从一开始就是不同的 token 类型不可能混用。这解释了两件事。第一为什么关键字不能做变量名——不是语义分析阶段拒绝它而是词法阶段就给它分配了另一个 kind语法分析器看到int int 10;直接报语法错误。第二为什么main不是关键字。main只是一个有特殊约定的函数名编译器不把它当保留字所以你可以定义一个叫main的变量虽然会和你自己的入口函数打架这在语法上是合法的。标准里这类保留字大约有九十来个加上 C20 的concept、requires、co_await、co_return、co_yield、consteval、constinit、char8_t等新成员数量还在涨。C20 还引入了「上下文相关关键字」比如final和override它们只在特定位置才有特殊含义其他位置仍然是合法标识符——这是语言委员会为了不破坏老代码做的妥协也说明关键字表本身是一个会随版本演化的东西。3.3 标识符规则背后是词法分析器的状态机「只能由字母、数字、下划线组成第一个字符必须是字母或下划线区分大小写」——这三条规则不是随便定的它们对应词法分析器里一个极简的有限状态机。分析器读到第一个字符如果它是字母或下划线就进入「标识符状态」开始累积只要后续字符是字母、数字或下划线就继续累积一旦遇到别的字符就结束这个 token切到下一个状态。整个过程一遍扫描不需要回退。如果允许数字开头就会和数字字面量的解析冲突——读到123时分析器没法立刻判断这是数字还是标识符的开头得往前看更多字符才能定最坏情况下要回退破坏了一遍扫描的性质。区分大小写则直接对应符号表的实现方式符号表用字符串做键count和Count是两个不同的字符串自然就是两个不同的条目。C 没有做大小写折叠省掉了每次查表都要转换的开销也避免了一堆歧义。「见名知意」这条建议听起来像鸡汤其实有实际后果。编译器生成的符号名会经过名字修饰name mangling把命名空间、类名、参数类型都编码进去最终出现在目标文件里。名字起得清楚链接错误时那串乱码还能勉强读懂起成a、b、c链接器报错的时候你连是哪个函数都找不到。这是血泪经验换来的规范。4. 表达式求值a和a到底差在哪条指令上运算符是 C 里最像数学、也最容易出玄学的地方。10 / 3得 3 而不是 3.333a * 10和a * 10结果不同的两边不一定会执行——这些都不是「语言怪癖」每一条背后都有明确的实现理由。4.1 整型除法为什么向零截断整数除法在 C11 之前商向哪个方向取整是实现定义的有的编译器向零截断有的向负无穷截断。C11 定了一刀商一律向零截断余数的符号与被除数相同。-10 / 3得-3不是-4-10 % 3得-1不是2。#include iostream using namespace std; int main() { int a 10, b 3; cout a / b endl; // 3小数部分被丢弃 cout a % b endl; // 1 int c -10; cout c / b endl; // -3向零截断不是 -4 cout c % b endl; // -1余数符号跟随被除数 // 恒等式(a / b) * b a % b a 对任何合法整数都成立 cout (c / b) * b c % b endl; // -10 // 除数为 0 是未定义行为不是异常程序可能直接崩也可能算出垃圾 int zero 0; // cout a / zero endl; // 不要这么干 // 两个浮点数可以除结果仍是浮点 cout 0.5 / 0.25 endl; // 2 // 浮点数不能取模 // cout 3.14 % 1.1 endl; // 编译错误 return 0; }向零截断这个选择不是随便定的。x86 的idiv指令做的就是向零截断编译器如果选了别的方向每次整数除法都要额外插几条修正指令代价太大。更重要的是它保住了(a / b) * b a % b a这个恒等式取模运算的很多算法比如哈希表的索引计算、循环缓冲区的下标回绕都依赖这条性质。整数除零是未定义行为不是异常这一点和 Java、Python 都不一样。编译器可以假设除零永远不会发生然后据此做优化——真发生了程序的行为无法预测。浮点数除零则是良定义的会得到inf或nan因为 IEEE 754 专门为这种情况留了编码。取模只对整型有效3.14 % 1.1直接编译不过。浮点数想求余要用std::fmod那是标准库函数不是运算符。4.2 自增自减左值与右值的分水岭a和a都会让a加 1差别在于表达式本身的值前置返回加完之后的值后置返回加之前的值。这个差别在单独成句时看不出来一旦参与更大的表达式就露出原形。#include iostream using namespace std; // 用一个玩具类观察前置和后置的真实成本差异 class Counter { public: int v; Counter(int x) : v(x) {} // 前置直接改自己返回引用零拷贝 Counter operator() { v; return *this; } // 后置先拷一份旧的再改自己返回旧值副本 Counter operator(int) { Counter old *this; // 这次拷贝是躲不掉的 v; return old; } }; int main() { int a 10; int b a * 10; // 先用 10 参与乘法再让 a 变 11 cout a a , b b endl; // 11, 100 int c 10; int d c * 10; // 先让 c 变 11再用 11 参与乘法 cout c c , d d endl; // 11, 110 Counter x(0); Counter y x; // 调用前置版本y 是 x 的拷贝 Counter z x; // 调用后置版本z 存的是加之前的值 return 0; }对内置类型a和a在开启优化后生成的机器码几乎一样因为编译器知道那个临时副本马上就会被丢掉。但对类类型a必须真的构造一个副本出来这个副本要调用拷贝构造函数、可能要分配内存、可能抛异常。这就是为什么遍历器的惯用写法是it而不是it——在泛型代码里你没法假设it是内置类型只能按最坏情况写。后置版本的那个int参数是个纯粹的语法标记叫「哑元参数」编译器靠它区分前置和后置两个重载。调用x时编译器自动传 0 进去你不需要也不应该手动传。这个设计有点丑但它是 C 早期为了兼容 C 又不想加新关键字的产物一直留到了今天。还有一个容易忽略的点a返回的是一个右值旧值的副本不能被赋值a返回的是a本身是左值可以出现在赋值号左边。对内置类型这条在 C11 之后被进一步收紧a 5直接编译错误。4.3 短路求值不只是逻辑运算和||的求值顺序是由标准强制规定的先算左边左边为假就整个表达式为假右边根本不执行||先算左边左边为真就整个为真右边不执行。这个特性叫短路求值它是 C 和 C 里少数几个把求值顺序写进语言规范的地方。#include iostream using namespace std; bool check(int* p) { cout check 被调用了 endl; return p ! nullptr; } int main() { int* p nullptr; // 短路左边为假右边压根不执行安全 if (p ! nullptr *p 0) { cout 不会走到这里 endl; } // 反过来写就是空指针解引用未定义行为 // if (*p 0 p ! nullptr) { ... } // 函数调用也能被短路掉 if (p ! nullptr check(p)) { cout check 不会被调用 endl; } int a 0, b 0; // 常见误用把 当成「两个都要执行」 (a 0) (b 5); // b 确实被赋成 5 了因为左边为真 (a ! 0) (b 9); // 左边为假b 保持 5没有被赋成 9 cout b b endl; // 5 return 0; }短路求值最大的实际价值是安全。判空后再解引用这种模式只有靠短路才能写成一行p ! nullptr *p 0顺序反过来就是灾难。标准库里的很多惯用法都建立在这上面比如if (i v.size() v[i] target)下标检查在前访问在后。代价是副作用。如果一个表达式里有副作用赋值、自增、函数调用又被短路掉了那个副作用就不会发生。上面代码里(a ! 0) (b 9)这一行b 9没有执行b停在 5。初学者经常在这里翻车以为两边总会算一遍。位运算和|没有短路语义两边都会求值这也是它们和逻辑运算符的关键区别之一。4.4 比较运算返回bool而不是int在 C 语言里比较运算符的结果类型是int真值是 1假值是 0。C 引入了bool作为独立的内置类型比较运算符和逻辑运算符的结果类型都是bool。这个改动看起来小影响却不小。#include iostream using namespace std; int main() { int a 10, b 20; cout (a b) endl; // 0 cout (a ! b) endl; // 1 cout (a b) endl; // 1 // boolalpha 让 bool 按 true/false 打印而不是 1/0 cout boolalpha; cout (a b) endl; // false cout (a b) endl; // true // sizeof 的差别bool 是 1 字节int 通常是 4 字节 cout noboolalpha; cout sizeof(a b) endl; // 1C 里会是 4 cout sizeof(int) endl; // 4 return 0; }bool让函数重载能区分「返回布尔判断」和「返回整数结果」也让模板元编程里能做类型分派。用int表真假的年代vectorint和vectorbool没法区分语义有了boolstd::vectorbool才能做那个著名的位压缩特化虽然它也因此成了标准库里最具争议的容器之一。cout (a b)默认打印 1 或 0 而不是 true 或 false这是operator对bool的重载决定的。想要人类可读的输出就得打开boolalpha操纵符。没打开的时候1 和 0 在视觉上和整数一模一样调试时容易看错——尤其是某个函数本该返回比较结果实际却返回了整数值两种情况下打印出来都是 1。5. 踩坑排查入门讲义里那几行能跑但埋雷的代码入门阶段最危险的不是编译不过的代码而是能跑、结果看起来也对、但埋着未定义行为或者平台依赖的代码。下面五条是我在实际带人过程中反复见到的每一条都值得停下来想一想。5.1 局部变量不初始化打印出一个「随机数」现象声明int a;之后直接cout a有时打印 0有时打印 4198320 之类的怪数换台机器结果又变了。原因局部变量分配在栈上栈空间是复用的那块内存里残留着上一个函数留下的数据。C 标准明确规定读取未初始化的自动变量是未定义行为编译器有权假设这件事不会发生据此做优化。所以「打印出一个随机数」已经是比较温和的后果更糟的情况是优化器把这个变量整个优化掉或者让分支走向不可预测。解决所有变量在定义时就给初始值这是讲义里反复强调的规矩虽然它不是编译器的硬性要求而是血泪经验。C11 之后更推荐用花括号初始化int a{};它对窄化转换会报错也能顺带初始化数组和结构体。5.2system(pause)换个平台就编译不过现象在 Windows 上跑得好好的程序拿到 Linux 上编译报错或者运行时提示命令不存在。原因system调用的是操作系统的 shellpause是 Windows 命令解释器的内建命令Linux 上没有这个东西。这一行的本意是让控制台窗口停住别让程序一闪而过属于 IDE 里直接运行时的小技巧不是程序逻辑的一部分。解决调试期间想暂停用cin.get()读一个字符它是标准库函数跨平台。真正发布的程序根本不需要这一行——控制台程序在终端里运行时输出本来就会留在终端里。把system(pause)和return 0一起当成模板抄进每个文件是入门阶段最常见的机械模仿。5.3cin读取失败后后面所有输入全部失效现象用cin a读整数用户手滑输入了一个字母程序接下来的所有cin全部被跳过变量保持原值程序像卡住了一样。原因cin是带状态的流对象。读取失败时它会置上failbit此后所有输入操作直接返回不再尝试读取。更麻烦的是那个非法字符还留在缓冲区里就算你清了failbit下一次读还会撞上同一个字符。解决读之前检查if (!(cin a))失败时用cin.clear()清状态位、cin.ignore()丢掉缓冲区里的残留字符再让用户重新输入。这套组合拳在写交互式程序时几乎是必备的讲义里通常只讲cin 的成功路径失败路径要自己补上。5.4 用比较两个浮点数现象明明算出来应该是 1.0sum 1.0却返回 false打印出来看着就是 1。原因十进制小数在二进制里大多是无限循环的0.1存进float或double时已经被截断十次累加之后的误差虽然微小但足以让两个值不再严格相等。cout默认只打印 6 位有效数字把误差藏起来了看起来「明明一样」。解决比较浮点数要判断差值的绝对值是否小于一个容差fabs(a - b) 1e-6这种写法。容差取多大取决于业务精度要求不是固定的。更根本的做法是尽量避免在需要精确比较的场景用浮点金额用整数分做单位比例用有理数表示。5.5 把sizeof(指针)当成数组长度现象在main里sizeof(arr) / sizeof(arr[0])算出 10把同样的代码搬进一个接收数组参数的函数结果变成 2 或 1。原因数组作为函数参数传递时会退化成指向首元素的指针函数内部的arr类型是int*而不是int[10]sizeof拿到的是指针大小64 位平台是 8除以sizeof(int)的 4 得到 2。这个退化是 C 语言留下来的设计C 为了兼容一直保留着。解决要么在调用方算好长度传进去要么用std::array或std::vector它们的.size()不受传参影响。现代 C 里裸数组作为函数参数基本应该绝迹用std::spanC20或者const std::vectorT都行。这条坑的本质是「静态类型信息在函数边界上丢失了」理解这一点之后很多类似的退化问题都能提前预判。6. 抽象的成本从内置类型到类对象C 的取舍在哪里C 有一句被引用烂了的话叫「零开销抽象」意思是你不用的东西不该付代价你用的东西不该比手写更差。这句话经常被当成营销口号其实它有非常具体的落点编译器必须能证明抽象层在生成代码时被完全消解掉否则这个抽象就不合格。拿变量、结构体、类这三层来看开销是逐层叠加的但每一层都有明确的消解机制。层次典型写法内存布局运行期开销来源能否被消解内置类型int a 10;4 字节无额外结构无——结构体struct P { int x; char c; };字段加对齐填充填充字节浪费空间调整字段顺序可缓解无虚函数类class C { int v; public: int get(); };与结构体完全相同无成员函数不占对象空间成员函数调用可内联有虚函数类带virtual的类多一个虚表指针每个对象 8 字节每次调用一次间接跳转无法消解只能 devirtualize关键的一行是第三行普通成员函数不占用对象空间也不产生调用开销。c.get()在编译后和get(c)是一样的函数体还能被内联展开。这就是std::vector只比裸指针加长度多几个字节的原因——它的size()、operator[]、迭代器在优化后基本都消失了。代价出现在第四行。一旦类里有了虚函数每个对象就要背一个虚表指针每次调用要走一次间接跳转而且编译器很难内联。这不是实现缺陷是运行期多态的必然成本调用哪个函数要到运行时才能确定编译器就没法在编译期把函数体塞进去。想要多态又不想要这个成本只能用模板做编译期多态代价是代码膨胀和编译时间。所以取舍很清楚小对象、频繁创建销毁的类型别随便加虚函数需要运行期替换行为的类型比如插件接口、跨模块的基类虚函数是唯一选择。讲义的入门部分把int、char、bool的字节数讲清楚核心部分把运算符和表达式的求值规则讲清楚提高部分最终要落到这个判断上——你能不能一眼看出某段代码的抽象层在编译后还剩下什么。我自己的习惯是每写一个新类先问一句「这个类型的对象会不会被大量创建」会的话就检查一遍有没有虚函数、有没有大的成员、字段顺序能不能再压一压。这个习惯是被一次内存暴涨逼出来的从那以后每次定类型布局我都强制走一遍sizeof验证而不是凭感觉估。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →