ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

《解锁C++进阶密码:引用补充与内联函数、nullptr 核心用法》

《解锁C++进阶密码:引用补充与内联函数、nullptr 核心用法》 前言从 C 转到 C 的人往往会在三个看似简单的地方栽跟头引用reference、内联函数inline function、以及空指针常量null pointer constant。它们都是 C 对 C 既有机制的改造件语法上跟 C 长得很像语义上却差得很远。常见的误解有三个。第一认为引用就是自动解引用的指针。这句话在实现层面大致成立多数编译器确实用地址实现引用但语义层面完全不同引用必须初始化、不能重新绑定、没有空引用这个概念。第二认为inline的作用是让编译器把函数展开。这是它最不重要的作用inline真正被标准规定的作用是放宽单一定义规则ODR让你能把函数定义写进头文件。第三认为nullptr只是NULL的一个好看点的写法。实际上nullptr有独立的类型std::nullptr_t它解决了NULL在重载决议overload resolution里选错重载的老问题。本文按引用补充、内联函数、nullptr三块来讲每块都给出能直接编译的例子并单独列出真会踩的坑。示例以 C17 为基准GCC 13 / Clang 17 / MSVC 19.3x 都能编过。一、引用的补充绑定、生命周期与返回1.1 引用的三条硬规则引用在语义上就是对象的别名标准给了它三条不可违背的规则必须初始化不存在未绑定的引用也因此不存在空引用。写int r;是编译错误。一旦绑定不能改绑。r x;是对被引用对象赋值不是让r指向x。不能有引用的引用、引用的数组、指向引用的指针。但可以通过模板的类型推导产生引用折叠reference collapsing这里不展开。1.2 const 引用与临时对象const T可以绑定到右值临时对象此时临时对象的生命周期被延长到该引用的作用域结束。这条规则只对直接绑定生效直接绑定const std::string r makeString();—— 生命周期延长安全。通过函数返回值拿到引用 —— 不延长悬垂。1.3 返回引用是一种契约函数返回引用等于对调用者承诺我引用对象的生命周期由你负责保证。返回局部变量的引用是典型的悬垂引用dangling reference后续任何读写都是 UB标准不保证任何行为。下面是贯穿全篇的完整示例引用、内联函数、nullptr三部分都在里面。读者可以先照着编译运行一遍后文再逐条拆解语法与陷阱。// ref_inline_nullptr.cpp —— C17GCC 13 / Clang 17 / MSVC 19.3x #include cstddef // std::nullptr_t #include iostream #include string // ---------- 引用 ---------- void swap_by_ref(int a, int b) { int t a; a b; b t; } class Counter { public: int value() { return n_; } // 返回内部成员的引用生命周期由对象保证 int value() const { return n_; } private: int n_ 0; }; // ---------- 内联函数 ---------- inline int add(int a, int b) { return a b; } // 允许多个翻译单元各自定义一份 inline constexpr double kPi 3.141592653589793; // C17 的 inline 变量 struct Widget { int get() const { return v_; } // 类内定义的成员函数隐式 inline int v_ 0; }; // ---------- nullptr ---------- void pick(int) { std::cout pick(int)\n; } void pick(char*) { std::cout pick(char*)\n; } int main() { int x 1, y 2; swap_by_ref(x, y); std::cout x y \n; // 2 1 const std::string sr std::string(temp); // 临时对象生命周期延长 std::cout sr.size() \n; // 4 Counter c; c.value() 42; // 通过引用写入内部状态 std::cout c.value() \n; // 42 std::cout add(2, 3) kPi \n; Widget w; w.v_ 7; std::cout w.get() \n; // 7 pick(0); // 实参是 int精确匹配 pick(int) pick(nullptr); // nullptr 只能转成 char*所以进 pick(char*) std::cout std::boolalpha (nullptr (void*)0) \n; // true }编译命令三者任选g -stdc17 -Wall -Wextra -O2 ref_inline_nullptr.cpp -o demo clang -stdc17 -Wall -Wextra -O2 ref_inline_nullptr.cpp -o demo二、内联函数inline 到底做了什么很多人只记得inline 是给编译器的展开建议但标准里inline的规范作用是 ODR 例外被声明为inline的函数、变量可以在多个翻译单元中重复定义只要每个定义逐 token 相同。这样才能把函数体直接写进头文件被多个.cpp包含而不报重复定义multiple definition链接错误。由此推论类内定义的成员函数隐式是inline。类外定义要写成inline int Foo::bar() { ... }否则头文件被多个 TU 包含就链接冲突。inline是否真的展开标准不管编译器可以完全忽略。真正决定展不展开的是优化器-O2、/O2和内联代价模型。取inline函数的地址时标准要求所有翻译单元中地址相同对具有外部链接的 inline 函数而言这是 ODR 例外的直接后果。C17 新增了inline 变量inline可以修饰命名空间作用域的变量让它在头文件里被定义且全局唯一。典型用途是把类的静态数据成员写成inline static省掉类外定义。三、nullptr一个独立的类型3.1 为什么 NULL 会出事NULL是一个宏它的展开由实现决定传统上是0MSVC 的cstddef里就是0GCC 的 C 头文件里则常用编译器内建符号__null。不管哪种NULL的类型都是整数或能隐式转成整数的特殊类型这就会在重载决议里出问题void f(int); void f(char*); f(NULL); // 极可能选中 f(int)而不是读者以为的 f(char*)nullptr是 C11 引入的关键字不是宏类型是std::nullptr_t定义在cstddef规则清楚得多表达式类型能隐式转成指针吗能隐式转成整数吗是关键字吗0int是空指针常量是否NULL实现定义常为int或long是是否是宏nullptrstd::nullptr_t是任意指针 / 成员指针否只有bool例外是(void*)0void*是C 里void*不再隐式转T*否否3.2 需要注意的实现细节sizeof(nullptr)等于sizeof(std::nullptr_t)而std::nullptr_t的大小是实现定义的。不要假设它是 8。decltype(nullptr)就是std::nullptr_t。nullptr转换到bool得到false所以if (!p)与if (p nullptr)在语义上一致。std::nullptr_t可以作为模板的实参类型也可以安全地跨函数传递。常见坑点坑 1返回局部变量的引用。❌ int bad() { int local 1; return local; } // 悬垂引用 ✅ int good() { int local 1; return local; } // 按值返回引用一旦悬垂之后的读写都是 UB标准不保证任何行为编译器也不一定报错-Wall常能警告但不可依赖。坑 2以为 const 引用总能延长生命周期。❌ const std::string bad() { return std::string(x); } // 返回时已悬垂 ✅ std::string good() { return std::string(x); }坑 3把引用成员塞进类里。struct S { ❌ int r; // 类内不能像这样默认初始化 - 默认构造被删除 ✅ S(int v) : r(v) {} // 必须靠构造函数初始化列表绑定 int r; };另外含引用成员的类拷贝赋值运算符会被隐式删除引用不能改绑S a b;这类代码会在意想不到的地方编译失败。坑 4把宏当内联函数用。❌ #define ADD(a, b) a b // ADD(1,2) * 3 变成 1 2 * 3 ✅ inline int add(int a, int b) { return a b; }宏还有参数被求值多次的问题ADD(i, j)会自增两次。坑 5用 NULL 调用重载函数。❌ f(NULL); // 走了 f(int)多半不是本意 ✅ f(nullptr); // 明确走指针版本坑 6以为 inline 一定能展开或以为写了 inline 就万无一失。❌ 在 .h 里写 int helper() { return 1; } // 被两个 .cpp 包含 - 链接期重复定义 ✅ 在 .h 里写 inline int helper() { return 1; }反过来想强制展开不能靠inlineGCC/Clang 是__attribute__((always_inline))MSVC 是__forceinline都是编译器扩展不是标准设施。坑 7if (p nullptr)与if (p nullptr)。❌ if (p nullptr) { ... } // 合法代码把 p 置空条件恒为 false编译无警告 ✅ if (p nullptr) { ... }建议开启-WallGCC/Clang 对无括号的赋值作条件通常有警告或统一写if (!p)。要比较时把常量写在左边nullptr p也能防误写但这只是风格约定。坑 8把 nullptr 传给可变参数。❌ printf(%p, nullptr); // 可变参数不做类型转换UB ✅ printf(%p, (void*)nullptr); // 显式转换可变参数variadic没有类型信息std::nullptr_t不是指针类型直接传进去是 UB。同理std::nullptr_t传给...时也不能期待它变成指针。总结主题标准怎么说常见误区实践建议引用必须初始化、不可改绑、无空引用当成自动解引用的指针参数用引用可选参数用指针或std::optionalconst 引用直接绑定临时对象时延长生命周期以为函数返回引用也延长返回引用前确认被引用对象活的更久inline 函数放宽 ODR允许多 TU 同一定义以为等于强制展开头文件里的自由函数定义一律加inlineinline 变量C17 起可用头文件可定义与static混用导致每 TU 一份头文件里的常量优先inline constexprnullptr类型std::nullptr_tC11 起以为和NULL等价新代码一律用nullptr不写NULL一句话记忆引用是有契约的别名inline是ODR 的通行证而不是展开指令nullptr是有类型的空指针。这三件事背后是同一条主线——C 用类型系统把 C 里靠约定维持的东西变成了编译器强制检查的规则。
返回列表