目录
命名空间
命名空间是 C++ 用于隔离标识符名称的语法机制,本质是划定一块独立作用域,将变量、函数、类等标识符收纳在这个作用域内,用来解决多文件协作、引入第三方库时产生的命名冲突问题。命名空间支持嵌套定义,同一个命名空间也可以分段写在多个位置,编译器会将同名命名空间的内容合并为同一个;命名空间仅在编译期生效,本身不占用内存,还可以为较长的命名空间定义别名,简化代码书写。命名空间内部的成员不会自动暴露到外部,访问成员时需要通过命名空间名称进行限定。
命名空间主要用于大型项目开发,在多人协作或者引入多个第三方库的场景下,不同模块、不同库之间很容易出现同名的类或函数,使用命名空间可以将各自的名称隔离开,避免名字冲突,同时也能够对工程代码按模块进行组织管理,C++ 标准库的所有接口就统一放置在 std 命名空间中。
访问命名空间成员有三种常用方式,第一种是直接使用命名空间名加作用域解析符进行限定访问,不会污染全局命名空间;第二种是 using 声明,仅将指定的单个标识符引入当前作用域;第三种是 using 指令,一次性引入命名空间内全部名字,书写便捷但容易造成命名污染。
使用命名空间时存在不少需要留意的地方,不要在头文件中使用 using namespace 指令,头文件会被多个源文件包含,会大范围引入命名空间标识符,极易引发命名冲突。使用 using 声明或者 using 指令时,其作用范围仅限于当前作用域,不会向外扩散。命名空间只隔离名字,命名空间内部定义的全局变量依旧拥有全局生命周期。工程开发中不建议滥用 using namespace std,优先使用 std:: 限定的方式访问标准库成员,降低命名冲突的风险。
cpp
// Stack.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
// 多文件中定义同名namespace,编译器会自动合并,视为同一个命名空间
namespace bit
{
typedef int STDataType;
// 动态顺序栈结构体
typedef struct Stack
{
STDataType* a; // 动态数组,存放栈元素
int top; // 栈顶标记,代表下一个要入栈的位置
int capacity; // 数组总容量
}ST;
// 栈接口声明
void STInit(ST* ps, int n);
void STDestroy(ST* ps);
void STPush(ST* ps, STDataType x);
void STPop(ST* ps);
STDataType STTop(ST* ps);
int STSize(ST* ps);
bool STEmpty(ST* ps);
}
cpp
// Stack.cpp
#include"Stack.h"
namespace bit
{
// 栈初始化,预开辟n个元素的空间
void STInit(ST* ps, int n)
{
assert(ps);
ps->a = (STDataType*)malloc(n * sizeof(STDataType));
ps->top = 0;
ps->capacity = n;
}
// 入栈
void STPush(ST* ps, STDataType x)
{
assert(ps);
// 判断栈是否已满,满则扩容
if (ps->top == ps->capacity)
{
printf("扩容\n");
// 空栈初始给4,否则容量翻倍
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->a = tmp;
ps->capacity = newcapacity;
}
ps->a[ps->top] = x;
ps->top++;
}
// 其余栈接口:STPop、STTop、STSize、STEmpty、STDestroy 省略
}
cpp
// Queue.h
#pragma once
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
namespace bit
{
typedef int QDataType;
// 链式队列节点结构
typedef struct QueueNode
{
int val;
struct QueueNode* next;
}QNode;
// 队列结构体,保存头尾指针与元素个数
typedef struct Queue
{
QNode* phead;
QNode* ptail;
int size;
}Queue;
// 队列接口声明
void QueueInit(Queue* pq);
void QueueDestroy(Queue* pq);
void QueuePush(Queue* pq, QDataType x); // 入队列
void QueuePop(Queue* pq); // 出队列
QDataType QueueFront(Queue* pq);
QDataType QueueBack(Queue* pq);
bool QueueEmpty(Queue* pq);
int QueueSize(Queue* pq);
}
cpp
// Queue.cpp
#include"Queue.h"
namespace bit
{
// 队列初始化
void QueueInit(Queue* pq)
{
assert(pq);
pq->phead = NULL;
pq->ptail = NULL;
pq->size = 0;
}
// 其余队列接口省略
}
cpp
// test.cpp
#include"Queue.h"
#include"Stack.h"
// 在全局作用域定义一套独立的栈结构,会和bit命名空间内的栈重名
typedef struct Stack
{
int a[10];
int top;
}ST;
// 全局版本栈的函数
void STInit(ST* ps){}
void STPush(ST* ps, int x){}
int main()
{
// 使用【全局】的ST结构体与函数
ST st1;
STInit(&st1);
STPush(&st1, 1);
STPush(&st1, 2);
printf("%d\n", sizeof(st1));
// 使用【bit命名空间】内的ST结构体与函数,用::访问
bit::ST st2;
printf("%d\n", sizeof(st2));
bit::STInit(&st2);
bit::STPush(&st2, 1);
bit::STPush(&st2, 2);
return 0;
}
C++的输入输出
C++ 的输入输出是基于流对象实现的,核心为标准输入流 cin 与标准输出流 cout,二者定义在 std 命名空间中,是对控制台输入输出的面向对象封装。cin 负责从键盘读取数据,cout 负责向控制台输出内容 ,还有 cerr 专门用来输出错误信息。相较于 C 语言的 scanf 和 printf,cin 与 cout 依靠运算符重载自动识别变量类型,不需要手动书写格式控制符,使用提取运算符 >> 完成输入,使用插入运算符 << 完成输出。
C++ 输入输出具备自动识别数据类型的能力,编译器会根据变量的实际类型完成读写处理,省去 C 语言中格式符的书写。输入输出运算符支持链式调用,运算符执行后会返回流对象本身,因此可以连续书写多个输入或者输出项。cin 默认将空格、换行符、制表符视作分隔符,读取数据时会自动跳过空白字符;cout 输出内容时不会自动换行,需要手动添加换行符号。endl 不仅实现换行,还会强制刷新输出缓冲区,而 '\n' 仅进行换行操作,不会主动刷新缓冲区。cin 与 cout 位于 std 命名空间内,使用时需要通过 std:: 限定,或是通过 using 引入命名空间 。流运算符支持重载,我们可以为自定义的类重载 >> 和 << 运算符,直接使用 cin、cout 读写自定义类型的对象。
C++ 标准输入输出多用于控制台的数据交互,适合程序基础的数据读取与结果打印,在笔试和日常小型开发中能够简化 IO 相关代码。当需要对自定义类对象进行读写时,重载流运算符后就可以直接使用 cin 和 cout 操作对象,这是 C 语言格式化输入输出不具备的特性。但在大批量数据读写场景下,cin 与 cout 默认读写速度低于 scanf 和 printf,处理大量数据时需要进行性能优化。
使用 C++ 输入输出时需要留意一些问题。cin 读取字符串时,遇到空格就会停止读取,如果需要读取包含空格的整行文本,应当选用 getline,不能直接使用 cin 读取字符串。cin 读取完成后,输入缓冲区会残留换行符,干扰后续 getline 读取,需要手动清理缓冲区。频繁使用 endl 会反复刷新缓冲区,造成性能下降,大量输出场景优先选择 '\n' 代替 endl。默认状态下 cin、cout 会和 C 语言的 stdio 保持同步,降低读写效率,处理海量数据时可以关闭同步来提升 IO 速度。当 cin 读取失败时,流对象会进入错误状态,后续所有输入操作都会失效,需要检测并重置流状态。cerr 输出的错误信息不经过缓冲区缓存,会直接打印,适合输出程序的报错信息。
缺省参数
缺省参数是 C++ 中函数声明时为形参指定默认值 ,调用函数时如果没有传入对应实参,编译器会自动使用预先设置的默认值来完成参数传递。
缺省参数分为全缺省和半缺省两种形式,全缺省代表所有形参都带有默认值,调用时可以不传任何实参;半缺省代表部分参数设置默认值,设置缺省参数必须从参数列表最右侧开始连续书写,不能间隔设置。缺省参数只在函数声明处进行定义,函数定义的位置不能再次给出缺省值 ,缺省值可以是常量或者全局变量,但不能使用局部变量作为缺省参数的值。
缺省参数常用于函数存在默认使用方案的场景,能够简化函数调用,减少重复传参。例如初始化函数,大部分场景下使用默认大小,只有特殊需求时才手动传入自定义数值,此时就适合使用缺省参数。在类的构造函数中,缺省参数也十分常用,可以用一个构造函数实现多种初始化方式,减少重载函数的数量。
使用缺省参数时需要注意,半缺省参数必须从右往左连续设置,不能出现有默认值的参数右侧跟着无默认值参数的情况。缺省参数不要同时在声明和定义两处书写,否则会引发编译报错 。缺省值不能使用局部变量,因为局部变量在函数调用时可能已经销毁,无法获取对应数值。当函数重载和缺省参数一起使用时,容易产生调用歧义,编译器无法判断该调用哪一个函数,需要尽量避免这种组合。
函数重载
函数重载是指在同一个作用域内,可以定义多个名称相同的函数,依靠参数列表的差异区分不同函数,编译器在编译阶段根据传入实参的类型、个数和顺序自动匹配调用对应的函数。
函数重载的特点是函数名保持一致,仅依靠参数列表进行区分,函数的返回值不能作为重载的判断依据。进行匹配时,编译器会按照参数类型的匹配优先级查找最合适的函数,必要时会发生隐式类型转换。重载的多个函数彼此相互独立,拥有各自独立的函数体,编译之后会被修饰成不同的函数符号,不会产生命名冲突。
函数重载多用于实现功能相近、处理数据类型或者参数数量不同的接口,能够提升代码可读性,让相同语义的操作使用同一个函数名。比如写一个加法函数,既可以处理整型,也可以处理浮点型,使用重载就不需要设计 addInt、addDouble 这类不同名称的函数。在类中,重载成员函数也十分常见,构造函数重载就是典型例子,支持多种对象初始化方式。
使用函数重载时需要注意,仅返回值不同无法构成重载,编译器无法仅凭返回值判断调用哪个函数。重载要求处于同一个作用域,不同作用域下同名函数不属于重载,会直接屏蔽外层同名函数。搭配缺省参数使用时容易产生调用歧义,编译器无法确定目标函数,应尽量避免。
引用
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间, 它和它引用的变量共用同一块内存空间。比如:水浒传中李逵,宋江叫"铁牛",江湖上人称"黑旋风";林冲,外号豹子头。


引用的特点是引用本身没有独立内存,对引用做修改等价于修改原变量。引用支持多级引用之外的连续别名定义,一个变量可以拥有多个引用。常引用可以限制通过引用修改原变量,用来保护数据。普通引用不能绑定临时变量,但是 const 引用可以绑定临时对象,延长临时变量的生命周期。引用和指针相比,引用更加安全,不存在空引用,使用语法更加简洁。
普通引用不能绑定临时变量,const 引用却可以绑定临时对象并延长临时变量生命周期,根源在于临时对象本身的属性与 C++ 的语法规则。临时对象是编译器自动生成的、没有名字的临时实体 ,属于右值,具备只读属性,生命周期很短,在当前表达式结束之后就会立刻销毁。
普通引用的设计初衷是用来修改绑定的实体,允许通过引用修改原值。如果允许普通引用绑定临时对象,就意味着代码可以尝试修改这个临时实体,但临时对象本身只是一个过渡性的中间结果,修改它没有实际意义,还容易引发逻辑错误,所以 C++ 语法直接禁止普通引用绑定右值临时对象。
而 const 引用是常量引用,承诺不会通过这个引用修改绑定的内容。既然保证只读,就不存在修改临时对象的风险,语法层面就允许 const 引用绑定临时对象。当临时对象绑定到 const 引用上时,编译器会延长这个临时对象的生命周期,临时对象的销毁时机不再是表达式结束,而是延迟到这个 const 引用的生命周期结束,因此我们可以在引用的作用域内正常使用这个临时对象。
举个简单例子,int& r = 10; 是非法的,10 是字面量临时对象,普通引用试图绑定右值,编译报错。而 const int& cr = 10; 合法,临时对象 10 的生命周期被延长,在 cr 的作用域内都可以读取这个值。
引用常用于函数参数传递,作为函数形参可以避免拷贝,提升程序运行效率,同时还可以在函数内部修改外部实参。如果不想修改实参,可以使用 const 引用。引用也可以用作函数返回值,当返回对象本身时,减少对象拷贝,但是不能返回局部变量的引用。范围 for 循环底层也依赖引用,在遍历容器时使用引用可以避免拷贝开销。
使用引用时需要注意,引用在定义阶段必须初始化,不存在没有绑定实体的引用。引用一旦绑定实体就无法更换绑定对象,不能像指针一样改变指向。不要返回局部变量的引用,函数结束后局部变量会销毁,引用会变成野引用。普通引用不能绑定字面量、临时对象,只有 const 引用才可以绑定临时变量。使用 const 引用时遵循权限原则,只能缩小权限,不能放大权限,不能通过常引用去修改原变量。
引用和指针的区别与联系
C++ 中指针和引用功能有重叠,但二者特性不同,不能相互替代。
1.在语法概念上,引用是变量的别名,不单独开辟内存空间,指针用于存储变量的地址,本身需要占用内存。
2.引用在定义的时候必须完成初始化,指针语法上不强制初始化,只是工程中建议初始化。
3.引用一旦绑定某个对象,就无法更换绑定目标,指针则可以随时修改指向,指向其他对象。
4.引用可以直接访问绑定的对象,指针需要进行解引用操作才能访问指向对象。
5.使用 sizeof 计算时,引用得到的是所绑定对象类型的大小,而指针永远是地址的字节大小,32 位平台占 4 字节,64 位平台占 8 字节。
6.指针容易产生空指针、野指针的问题,引用不存在空引用,使用起来相对更安全。
nullptr
nullptr 是 C++11 引入的空指针常量,用来表示空指针,专门替代原来的 NULL。
nullptr 的特点是它的类型是 std::nullptr_t,是专门的空指针类型,不会发生隐式类型转换为整型。而传统的 NULL 本质上是宏,一般被定义成 0,属于整型常量,在函数重载场景下容易产生歧义。nullptr 可以赋值给任意类型的指针,代表指针不指向任何有效内存,不能对 nullptr 进行解引用操作.
nullptr 主要用于给指针赋空值,判断指针是否为空。在函数重载场景下,使用 nullptr 可以避免 NULL 带来的二义性问题。在初始化指针、函数参数传空指针、类成员指针初始化等场景都推荐使用 nullptr。
使用 nullptr 时需要注意,不能对 nullptr 解引用,否则程序会崩溃。nullptr 是关键字,不需要头文件也可以直接使用,std::nullptr_t 是它对应的类型。不要把 nullptr 当成整数使用,它无法隐式转换成 int。在重载函数中,如果同时存在指针参数和 int 参数,传入 NULL 会匹配 int 版本,而传入 nullptr 会匹配指针版本,这也是优先选用 nullptr 的主要原因。