C++ 核心特性详解
C++ 作为一门兼容 C 语言特性且融入现代编程范式的语言,是系统开发与应用高性能应用的重要工具。本文聚焦其核心基础——引用、函数重载、nullptr、内联函数和 auto 等关键特性,帮你快速掌握入门要点,夯实基础。
函数重载
定义
重载在自然语言中意味着一个词有多种意思,可以通过上下文判断。函数重载则是 C++ 允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数或类型或类型顺序)不同,函数重载对返回值没有要求,即函数重载与返回值是否相同没有影响。常用来处理实现功能类似但数据类型不同的问题。
条件
参数类型不同
#include <iostream>
using namespace std;
void print(int num) {
cout << "void print(int num)" << endl;
}
void print(double num) {
cout << "void print(double num)" << endl;
}
int main() {
print(1);
print(1.1);
return 0;
}
函数参数类型不同,函数名字相同,构成函数重载。编译器会自动匹配类型。
参数数量不同
#include <iostream>
using namespace std;
void sum(int a, int b) {
cout << "void sum(int a, int b)" << endl;
}
void sum(int a, int b, int c) {
cout << "void sum(int a, int b, int c)" << endl;
}
int main() {
sum(1, 2);
sum(1, 2, 3);
return 0;
}
函数参数类型相同,函数名字相同,参数个数不同,构成函数重载。编译器会自动匹配类型。
注意,如果存在缺省参数调用时可以不传参,两个函数都可以,存在调用歧义。
#include <iostream>
using namespace std;
void f() {
cout << "f()" << endl;
}
void f(int a = 0) {
cout << "f(int a = 0)" << endl;
}
int main() {
f();
return 0;
}
函数名字相同,参数个数不同,构成函数重载。但会报错,是因为缺省参数调用时可以不用传参,两个函数都可以,存在调用歧义。
参数顺序不同
#include <iostream>
using namespace std;
void process(int a, double b) {
cout << "void process(int a, double b)" << endl;
}
void process(double a, int b) {
cout << "void process(double a, int b)" << endl;
}
int main() {
process(1, 1.1);
process(1.2, 1);
return 0;
}
函数参数类型相同,函数名字相同,参数顺序不同,构成函数重载。
注意,函数重载与函数的返回类型和参数名字是否相同没有关系。
#include <iostream>
using namespace std;
void f(int a, char b) {
cout << "f(int a, char b)" << endl;
}
void f(int b, char a) {
cout << "f(int b, char a)" << endl;
}
int main() {
f(1, 'x');
return 0;
}
函数参数类型相同,函数名字相同,函数参数名字不同,不构成函数重载,程序会报错。
C++ 支持函数重载的原因
这里以 Linux 环境为例演示 C 语言为什么不支持函数重载,C++ 为什么支持函数重载。
在 Linux 环境下,采用 gcc 编译完成后,函数名字的修饰没有发生改变,这意味着同名函数无法区分。而采用 g++ 编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中,这就是名称修饰(Name Mangling)。只要参数不同,修饰出来的名字就不一样,就支持了重载。如果两个函数的函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办法区分。
引用
定义
引用是一项重要特性,它为变量提供了一个别名,让你能够通过这个别名来访问和操作原始变量。编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
使用方法
- 引用是已存在变量的别名,通过在变量名前使用 & 符号声明。
- 引用必须在声明时初始化,且一旦初始化后不能再引用其他变量。
- 一个变量可以有多个引用。
- 引用在同一个域不能同名,在不同的域可以同名。
#include <iostream>
using namespace std;
int main() {
int a = 0;
int& b = a;
int& c = b;
int& d = a;
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
b++;
cout << a << endl;
cout << b << endl;
cout << c << endl;
cout << d << endl;
d++;
cout << a << endl;
cout << b << endl;
cout << c << endl;
cout << d << endl;
return 0;
}
在上面代码中 d = x 是赋值,而不是引用。赋值需要开辟空间,引用是多个变量在同一份空间。
使用场景
- 引用传递允许函数直接修改实参的值,避免值传递的拷贝开销。(输出型参数)
#include <iostream>
using namespace std;
void Swap(int* a, int* b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
void Swap(int& a, int& b) {
int tmp = a;
a = b;
b = tmp;
}
int main() {
int x = 0;
int y = 1;
cout << x << " " << y << endl;
//Swap(&x, &y);
Swap(x, y);
cout << x << " " << y << endl;
return 0;
}
- 通过引用传递指针与通过指针的指针传递。
#include <iostream>
using namespace std;
void Swap(int*& a, int*& b) {
int* tmp = a;
a = b;
b = tmp;
}
void Swap(int** a, int** b) {
int* tmp = *a;
*a = *b;
*b = tmp;
}
int main() {
int x = 0;
int y = 1;
int* px = &x;
int* py = &y;
cout << *px << " " << *py << endl;
Swap(px, py);
cout << *px << " " << *py << endl;
Swap(&px, &py);
cout << *px << " " << *py << endl;
return 0;
}
- 通过引用传递头指针,确保可以修改原指针。
#include <iostream>
using namespace std;
typedef struct ListNode {
struct ListNode* next;
int val;
} LTNode, *PLTNode;
void ListPushBack(PLTNode& phead, int x) {}
int main() {
PLTNode plist = NULL;
ListPushBack(plist, 1);
return 0;
}
- 引用做返回值
传值返回
#include <iostream>
using namespace std;
int Count() {
static int n = 0;
n++;
return n; //会产生临时变量,关注的是类型
}
int main() {
int ret = Count();
cout << ret << endl;
return 0;
}
传值返回时一定会生成临时变量。
传引用返回
#include <iostream>
using namespace std;
int& Count() {
static int n = 0;
n++;
return n;
}
int main() {
int ret = Count();
cout << ret << endl;
return 0;
}
传引用返回的是 n 的别名,n 的引用。不会产生临时变量。价值:减少拷贝,提高效率。
注意,永远不要返回局部变量的引用或指针。
#include <iostream>
using namespace std;
int& Count() {
int n = 0;
n++;
return n;
}
int main() {
int ret = Count();
cout << ret << endl;
return 0;
}
上面的代码中,返回了局部变量的引用,即返回了局部变量 n 的地址,但是当函数返回时栈帧被销毁,n 的内存空间不再有效,此时 ret 的值是不确定的。这是因为 Count 函数结束后,没有清理栈帧,ret 的结果侥幸是正确的,如果清理栈帧,ret 的结果就是随机值。如果要安全返回则需要谨慎使用静态变量、动态内存分配或者返回值。
总结:基本任何场景都可以使用引用返回。谨慎使用引用做返回值,出了函数作用域,对象不在了,就不能使用引用返回,否则还在,就可以使用引用返回。命名空间不会影响生命周期。
传值、传引用效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。 引用做参数可以提高效率 (大对象/深拷贝类对象)。大对象通常指占用大量内存或包含动态分配资源(如堆内存、文件句柄等)的对象。
#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 < 10000; ++i) {
TestFunc1(a);
}
size_t end1 = clock();
//以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i) {
TestFunc2(a);
}
size_t end2 = clock();
//分别计算两个函数运行结束后的时间
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main() {
TestRefAndValue();
return 0;
}
TestFunc1 的耗时会显著高于 TestFunc2,这是因为值传递需要频繁复制大对象,涉及大量内存操作。而引用传递只需传递指针,无需复制数据。
引用在顺序表的应用
#include <assert.h>
#include <iostream>
using namespace std;
typedef struct SeqList {
int a[100];
int size;
} SeqList;
int SLGet(SeqList* ps, int pos) {
assert(pos < 100 && (pos >= 0));
return ps->a[pos];
}
void SLModify(SeqList* ps, int pos, int x) {
assert(pos < 100 && (pos >= 0));
assert(ps);
ps->a[pos] = x;
}
int& SLAt(SeqList* ps, int pos) {
assert(pos < 100 && pos >= 0);
return ps->a[pos];
}
int& SLAt(SeqList& ps, int pos) {
assert(pos < 100 && pos >= 0);
return ps.a[pos];
}
int main() {
SeqList s;
//方法 1
SLModify(&s, 0, 1);
cout << SLGet(&s, 0) << endl;
//对第 0 个位置 +5
int ret = SLGet(&s, 0);
SLModify(&s, 0, ret + 5);
//方法 2
//引用 (读写)
SLAt(&s, 0) = 1;
cout << SLAt(&s, 0) << endl;
SLAt(&s, 0) += 5;
//方法 3
SLAt(s, 0) = 1;
cout << SLAt(s, 0) << endl;
SLAt(s, 0) += 5;
return 0;
}
在方法 1 中如果想要改变某一位置的值,需要使用 SLGet 和 SLModify 2 个函数,方法 2 只需要使用引用做返回值可以修改返回值。方法 3 是对方法 2 进行了简化。
常引用
常引用是指向常量对象的引用。它的主要作用是在引用对象时,禁止通过该引用来对对象的值进行修改,从而保证数据的安全性。
- 情况 1
#include <iostream>
using namespace std;
int main() {
//不可以
//引用过程中,权限不能放大
const int a = 0;
int& b = a;
return 0;
}
const 的主要功能是对对象的不变性进行声明,即 const 变量一旦被初始化,其值就不能再被更改。引用过程中权限不能放大。
#include <iostream>
using namespace std;
int main() {
//方法 1
//将引用 b 也声明为 const 类型
const int a = 0;
const int& b = a;
//权限保持一致或缩小
//方法 2
//如果需要修改值,就不要使用 const
int c = 0;
int& d = c;
//c 本身是变量
//方法 3
//使用常量引用绑定临时对象
const int& e = 42;
//常量引用可绑定临时对象
return 0;
}
- 情况 2
#include <iostream>
using namespace std;
int main() {
const int c = 0;
int d = c;
return 0;
}
这种情况可以,这是赋值,c 拷贝给 d,没有放大权限,因为 d 的改变不影响 c。
- 情况 3
#include <iostream>
using namespace std;
int main() {
int x = 0;
int& y = x;
//权限的平移
const int& z = x;
//缩小 z 的权限
return 0;
}
在引用过程中,权限可以缩小或平移,但不能放大。上面的代码中因为 const 修饰的变量的值不能发生改变,所以 z 不能 ++,x 可以 ++。
- 情况 4
#include <iostream>
using namespace std;
int main() {
double e = 1.11;
//int& rii = e;
//产生 int 的临时变量
const int& rii = e;
//临时变量具有常性
return 0;
}
上面的代码中发生了隐式类型转换,类型转换产生的临时对象默认具有常量性,因此只能用 const 引用绑定。发生类型转换(强制类型转换,截断,隐式类型转换),函数会产生临时变量。相同类型不产生临时变量。
- 情况 5
#include <iostream>
using namespace std;
int func1() {
static int x = 0;
return x;
}
int& func2() {
static int x = 0;
return x;
}
int main() {
int ret1 = func1(); //拷贝
int& ret1 = func1();
const int& ret1 = func1(); //权限的平移
int& ret1 = func2(); //权限的平移
const int& ret1 = func2(); //权限的缩小
return 0;
}
函数返回的是临时变量,临时变量具有常性。由传值返回变为引用,这是权限的放大。
总结:在 C++ 中,引用的权限不能超过原始对象的权限。也就是说,不能通过引用去获得比原始对象更多的修改权限。常量对象只能被常量引用,而变量对象则可以被视为常量引用或者非常量引用。
引用和指针的不同点
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求。
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
- 没有 NULL 引用,但有 NULL 指针。
- 在 sizeof 中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数 (32 位平台下占 4 个字节)。
- 引用自加即引用的实体增加 1,指针自加即指针向后偏移一个类型的大小。
- 有多级指针,但是没有多级引用。
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理。
- 引用比指针使用起来相对更安全,但引用不能完全代替指针。指针有野指针和空指针,但引用没有。
内联函数
定义
内联函数是一种特殊函数,通过编译器优化减少函数调用开销,提高执行效率。
基本语法
使用 inline 关键字声明,建议编译器将函数体直接替换调用处(类似宏展开):
inline int add(int a, int b) {
return a + b;
}
宏函数(C 语言) 优点:不需要建立栈帧,提高调用效率。 缺点:复杂,容易出错,可读性差,不能调试。
#define Add(x, y) ((x + y) * 10)
内联函数 VS 宏
| 特性 | 内联函数 | 宏 |
|---|---|---|
| 类型安全 | 遵循 C++ 类型检查 | 简单文本替换,无类型检查 |
| 调试支持 | 可调试 | 不可调试(预处理阶段展开) |
| 副作用 | 无 | 可能多次求值 |
| 代码膨胀风险 | 由编译器决定是否展开 | 强制展开,可能导致代码冗余 |
核心作用 —— 减少函数调用开销
普通函数调用流程:
- 保存调用位置上下文
- 跳转到函数地址执行
- 返回时恢复上下文
内联函数:直接将函数体代码复制到调用处,避免上述开销,适合短小频繁调用的函数。
编译器处理规则
inline 是建议,非强制:编译器可能忽略 inline 声明(如函数体复杂、递归调用等)。 展开条件:函数体短小,无循环、递归等复杂结构。 定义位置:内联函数必须在调用点前被定义(通常放在头文件中),否则链接时可能报错,内联函数的声明和定义不能分离。
auto 关键字
定义
auto 是一个功能强大的类型说明符,它能让编译器自动推导变量的类型,从而显著简化代码。
基本语法
auto 的基本语法是在声明变量时,用 auto 关键字替代具体的类型,编译器会根据初始化表达式自动推导变量的类型。可以使用 typeid 函数输出变量的类型。
#include <iostream>
using namespace std;
int main() {
auto x = 42; //推导为 int
auto y = 3.14; //推导为 double
auto z = "hello"; //推导为 const char*
auto& ref = x; //推导为 int&
const auto& cref = x; //推导为 const int&
cout << typeid(cref).name() << endl;
cout << typeid(ref).name() << endl;
return 0;
}
auto 在数组中的应用
#include <iostream>
using namespace std;
int main() {
int arr[] = {1, 2, 3, 4, 5};
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {
arr[i] *= 2;
}
//for (int* p = arr; p < arr + sizeof(arr) / sizeof(arr[0]); p++)
//{
// cout << *p << " ";
//}
//cout << endl;
//范围 for
//依次取数组中的数据赋值给 e
//自动迭代,自动判断结束
//数组都可以
for (auto e : arr) {
//e *= 2;
cout << e << " ";
}
cout << endl;
for (auto& e : arr) //auto 可以适用于任何数组,也可以使用 int
{
e *= 2;
}
for (auto e : arr) {
cout << e << " ";
}
return 0;
}
关键点总结 引用是修改的关键 使用 for(auto& e : arr)(带引用)可直接修改原数组元素。 使用 for(auto e : arr)(值拷贝)只能读取数据,修改无效。
auto 的优势 自动推导元素类型,无需手动指定。 数组类型变化时无需调整,代码更健壮。
变量名随意 e 可替换为任意合法标识符。
范围 for 循环使用的条件
for 循环迭代的范围必须是确定的 对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供 begin 和 end 的方法,begin 和 end 就是 for 循环迭代的范围。
#include <iostream>
using namespace std;
void TestFor(int array[]) {
for (auto& e : array) {
cout << e << endl;
}
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
TestFor(arr);
return 0;
}
当数组作为函数参数传递时,它会自动退化为指向首元素的指针。指针没有保存数组大小信息(丢失了 sizeof(array)),范围 for 需要知道数组的起始和结束位置。
指针空值(nullptr)
- 在 C++ 里,NULL 一般被定义成整数 0,它的本质是个整数类型。
- nullptr:这是 C++11 新引入的关键字,它的类型是 std::nullptr_t。nullptr 能够隐式地转换为任意指针类型,但不能转换为整数类型。
- 在使用 nullptr 表示指针空值时,不需要包含头文件,因为 nullptr 作为新关键字引入的。
- sizeof(nullptr) 与 sizeof((void*)0) 所占的字节数相同。
总结
函数重载、引用、内联函数、auto 和 nullptr 是 C++ 基础核心。重载让同名函数依参数适配场景;引用以别名简化操作、提升效率;内联平衡调用开销与复用;auto 减少类型声明冗余;nullptr 规范空指针。这些特性体现 C++ 在兼容与创新间的平衡,既承 C 语言高效,又添安全便捷。吃透并活用它们,是进阶高阶特性的基石,也能轻松应对面试基础考点,助你扎实迈入 C++ 之门。


