跳到主要内容
C++ 入门:引用、内联函数与 C++11 新特性详解 | 极客日志
C++
C++ 入门:引用、内联函数与 C++11 新特性详解 C++ 引用作为变量别名共享内存,定义需初始化且不可重绑定。内联函数通过编译期展开减少调用开销,但可能增加代码体积,递归函数通常无法内联。C++11 引入 auto 关键字实现类型推导简化声明,配合指针或引用使用需注意显式符号。基于范围的 for 循环要求迭代对象支持 ++ 和 == 操作。nullptr 替代 NULL 消除重载歧义,明确表示空指针类型。掌握这些基础特性有助于编写高效、安全的现代 C++ 代码。
引用
引用概念
引用不是新定义一个变量,而是给已存在变量取了一个别名。编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。比如李逵,在家称为铁牛,江湖上人称黑旋风。
void TestRef () {
int a = 10 ;
int & ra = a;
cout << &a << endl;
cout << &ra << endl;
}
注意:引用类型必须和引用实体是同种类型的。
引用的特性
引用在定义时必须初始化。
一个变量可以有多个引用。
引用一旦引用一个实体,再不能引用其他实体。
#include <iostream>
using namespace std;
int main () {
int a = 10 ;
int & b = a;
int & d = a;
int c = 20 ;
b = c;
return 0 ;
}
常引用
const 引用就是常引用。当 const 引用绑定临时变量时,会延长临时变量的生命周期,使其与 const 引用的生命周期一致,直到该引用(如 rii)的生命周期结束。
使用场景
引用做参数
void Swap (int & left, int & right) {
int temp = left;
left = right;
right = temp;
}
输出型参数 :传参时不用担心值拷贝问题,因为形参和实参都指向同一实体。
提高效率 :传递大对象时,只需传递对象的引用(而非拷贝整个对象),避免了大对象拷贝带来的性能开销,从而提高程序运行效率。
引用做返回值 int & Add (int a, int b) {
int c = a + b;
return c;
}
int main () {
int & ret = Add (1 , 2 );
Add (3 , 4 );
cout << "Add(1, 2) is :" << ret << endl;
return 0 ;
}
上面代码有个很明显的问题:返回了局部变量的引用,局部变量出了函数就销毁了,这里返回的 ret 的值有两种可能性,如果函数结束栈帧销毁,但是没有清理栈帧,那么 ret 引用可能指向被引用的局部变量,如果栈帧被清理,那么 ret 指向的就是随机值了。
int & Count () {
static int n = 0 ;
n++;
return n;
}
返回个静态区的局部变量,静态区局部变量的生命周期是整个程序,所以无论用 int 接收还是用 int& 接收都没有任何问题。
1、接收返回引用的两种情况:
用普通变量接收:会拷贝静态变量当前的值;
用引用变量接收:这个引用会成为静态变量的别名。
2、返回引用的优势:
避免对象拷贝(尤其对大对象),提升性能;
可直接修改原始对象,支持链式操作。
3、返回引用的风险:
若被引用对象(比如栈区局部变量)被销毁,会形成'悬空引用',此时访问或修改会引发崩溃、数据错乱等未定义行为。
传引用、传值效率比较 以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
#include <time.h>
#include <iostream>
using namespace std;
struct A {
int a[10000 ];
};
void TestFunc1 (A a) {}
void TestFunc2 (A& a) {}
void TestRefAndValue () {
A a;
size_t begin1 = clock ();
for (size_t i = 0 ; i < 1000000 ; ++i) TestFunc1 (a);
size_t end1 = clock ();
size_t begin2 = clock ();
for (size_t i = 0 ; i < 1000000 ; ++i) TestFunc2 (a);
size_t end2 = clock ();
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
#include <time.h>
#include <iostream>
using namespace std;
struct A {
int a[10000 ];
};
A a;
A TestFunc1 () {
return a;
}
A& TestFunc2 () {
return a;
}
void TestReturnByRefOrValue () {
size_t begin1 = clock ();
for (size_t i = 0 ; i < 100000 ; ++i) TestFunc1 ();
size_t end1 = clock ();
size_t begin2 = clock ();
for (size_t i = 0 ; i < 100000 ; ++i) TestFunc2 ();
size_t end2 = clock ();
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main () {
TestReturnByRefOrValue ();
return 0 ;
}
指针和引用的区别 在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
int main () {
int a = 10 ;
int & ra = a;
cout << "&a = " << &a << endl;
cout << "&ra = " << &ra << endl;
return 0 ;
}
在底层实现上实际是有空间的,因为引用可能是按照指针方式来实现的(不同编译器的实现细节肯定不一样)。从汇编代码来看,底层是差不多的,但是也不能一概而论,毕竟每个编译器的实现不一样。
引用和指针的不同点
引用概念上定义一个变量的别名,指针存储一个变量地址。
引用在定义时必须初始化,指针没有要求。
引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
没有 NULL 引用,但有 NULL 指针。
在 sizeof 中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32 位平台下占 4 个字节)。
引用自加即引用的实体增加 1,指针自加即指针向后偏移一个类型的大小。
有多级指针,但是没有多级引用。
访问实体方式不同,指针需要显式解引用,引用编译器自己处理。
引用比指针使用起来相对更安全。
内联函数
内联函数是啥? 以 inline 修饰的函数叫做内联函数,编译时 C++ 编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
如何判断是否为内联函数? 在 Release 模式下,查看编译器生成的汇编代码中是否存在 call 指令。在 Debug 模式下,需要对编译器进行设置,否则不会展开(因为 Debug 模式下,编译器默认不会对代码进行优化)。
通常需要在项目属性中调整优化选项,例如将内联函数拓展修改成只适用于 _inline。
内联函数特性 核心是以空间换时间 —— 编译阶段用函数体替换调用,优势是减少调用开销、提升效率;缺陷是可能增大目标文件。
编译器的建议性:inline 是建议而非强制,编译器通常仅处理规模小、非递归、频繁调用的函数,大函数或递归函数会被忽略。
声明定义不分离:分开写会导致链接错误(跨源文件调用时更明显),因为内联函数无独立地址。
内部链接属性:不同源文件可定义同名内联函数,不会引发链接冲突。
常见问题
为啥内联函数可能会导致目标文件变大 内联函数会将函数体复制到每一个调用处,如果函数被多次调用,代码体积就会显著增加。
递归不能内联的核心原因 内联需编译时确定展开次数,而递归调用层次由运行时动态决定(依赖输入或状态),编译器无法预知,故无法安全展开。
宏的优缺点? 宏是预处理阶段的文本替换,速度快但缺乏类型检查,容易引发副作用。
内联函数的优缺点? 内联函数有类型检查,安全性高于宏,但受限于函数大小和编译器策略。
auto 关键字 (C++11)
auto 简介 在 C++11 中,auto 关键字被赋予了全新的含义 —— 作为编译期类型推导指示符。它不再表示'自动存储期的变量',而是让编译器根据变量的初始化表达式,自动推导出变量的实际类型。这一特性极大简化了复杂类型的声明,尤其在处理冗长的 STL 容器迭代器、模板类型等场景时,能显著提升代码的简洁性和可读性。
使用 auto 的核心要求是变量必须初始化,因为编译器需要通过初始化表达式才能完成类型推导。
int a = 10 ;
auto b = a;
auto c = 'c' ;
auto 的使用细则
auto 与指针和引用结合起来使用 声明指针类型时,auto 和 auto* 的效果完全一致,均会推导出指针类型:
int x = 20 ;
auto * p1 = &x;
auto p2 = &x;
声明引用类型时,必须显式添加 &,否则 auto 会推导出被引用对象的类型而非引用:
int y = 30 ;
auto & r = y;
auto r2 = y;
在同一行定义多个变量 当在同一行使用 auto 声明多个变量时,所有变量必须能被推导为相同类型,否则会编译报错。这是因为 auto 仅能推导出一种类型,无法同时适配多种不同类型:
auto 不能推导的场景
auto 不能作为函数的参数 编译器无法在编译期根据函数调用情况推导出参数的实际类型,因此 auto 不能用于函数形参的声明:
void func (auto param) { }
auto 不能直接用来声明数组 auto 无法推导出数组类型,因此不能直接用于数组的声明。若需简化数组相关的类型声明,可结合指针或引用间接实现:
int arr[] = {1 , 2 , 3 };
auto * p = arr;
基于范围的 for 循环 (C++11)
范围 for 的语法 在 C++98 中遍历数组需手动控制循环范围,如:
void TestFor () {
int array[] = {1 , 2 , 3 , 4 , 5 };
for (int i = 0 ; i < sizeof (array) / sizeof (array[0 ]); ++i)
array[i] *= 2 ;
for (int * p = array; p < array + sizeof (array) / sizeof (array[0 ]); ++p)
cout << *p << endl;
}
而 C++11 的范围 for 循环语法简洁,由冒号':'分为迭代变量和被迭代范围两部分,示例:
void TestFor () {
int array[] = {1 , 2 , 3 , 4 , 5 };
for (auto & e : array) e *= 2 ;
for (auto e : array) cout << e << " " ;
return 0 ;
}
它支持 continue 结束本次循环、break 跳出整个循环,与普通循环逻辑一致。
范围 for 的使用条件
for 循环迭代的范围必须是确定的 对于数组,范围是数组第一个元素到最后一个元素;对于类,需提供 begin 和 end 方法来界定迭代范围。如下代码因范围不确定会出问题:
void TestFor (int array[]) {
for (auto & e : array) cout << e << endl;
}
迭代的对象要实现 ++ 和 == 的操作 迭代过程依赖这些操作来控制迭代逻辑(此部分涉及迭代器知识,后续会详细讲解,现阶段了解即可)。
指针空值 nullptr (C++11)
NULL 的问题? NULL 本质是宏,在传统 C 头文件中定义如下:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void*)0)
#endif
#endif
即 NULL 可能被定义为字面常量 0,或无类型指针 (void*) 的常量。
void f (int ) {
cout << "f(int)" << endl;
}
void f (int *) {
cout << "f(int*)" << endl;
}
int main () {
f (0 );
f (NULL );
f ((int *)NULL );
return 0 ;
}
这里 f(0) 调用 f(int) 没问题,但 f(NULL) 由于 NULL 定义的模糊性(既像 0 又像指针),可能导致编译器匹配混乱,而 f((int*)NULL) 虽然明确转化为指针类型调用 f(int*),但这种写法不够简洁直观。
为啥引入 nullptr?
无需额外头文件 :nullptr 是 C++11 引入的新关键字,专门表示指针空值。使用它时,无需包含额外头文件,代码简洁性提升。
字节数特性 :在 C++11 中,sizeof(nullptr) 与 sizeof((void*)0) 所占字节数相同。这意味着 nullptr 在内存占用等底层特性上,和传统表示空指针的方式在字节层面有对应关系。
提升代码健壮性 :相比 NULL 可能带来的歧义,nullptr 明确表示指针空值。在函数重载等场景下,能让编译器准确匹配函数,减少错误发生概率,使代码更健壮。
总结来说,nullptr 作为 C++11 的新特性,解决了 C++98 中 NULL 表示指针空值的一些弊端,让指针空值的表达更清晰、准确,有助于写出更可靠的代码。
nullptr 类型? nullptr 的类型是 std::nullptr_t,它可以隐式转换为任何指针类型(包括对象指针、函数指针等),但不能转换为整数类型(这一点和 NULL 不同,NULL 可能被解析为整数 0)。
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online