![[Pasted image 20260808210409.png]]
1. 命名空间
#include<stdio.h> #include<stdlib.h> // 定义在全局域 int rand = 10; int main() { // 编译报错: “rand”: 重定义;以前的定义是“函数” printf("%d", rand); return 0; }#include<stdio.h> #include<stdlib.h> int main() { // 定义在函数局部域 int rand = 1; printf("%d", rand); // 访问全局变量 printf("%d", ::rand); return 0; }编译器查找一个变量/函数/类型的声明或定义时,默认逻辑是:先查找局部域,再查找全局域。
在 C++ 中,变量、函数以及类都是大量存在的,默认情况下它们的名称都存在于全局作用域中,这极易导致命名冲突。
因此,使用namespace的目的是对这些标识符的名称进行本地化,以避免命名冲突和名字污染。
1.1. C++ 中的域
C++ 中常见的域包括:
- 局部域 (Local Scope)
- 全局域 (Global Scope)
- 命名空间域 (Namespace Scope)
- 类域 (Class Scope)
变量的生命周期:
• 局部域 & 全局域:不仅影响查找,还会决定变量的生命周期(如局部自动变量,随栈帧创建和销毁)。
• 命名空间域 & 类域:只影响编译查找,不影响变量的生命周期。定义在namespace中的变量生命周期与全局变量一致(位于静态存储区,贯穿整个程序运行期)。
1.2.namespace
定义命名空间,需要使用namespace关键字。命名空间中可以定义变量、函数、类型等。
namespace namespace_name { //成员 }namespace的本质是定义一个命名空间域,这个域跟全局域各自独立,不同的域可以定义同名变量。
1.3. 命名空间的使用
编译时查找一个变量的定义/声明时,默认只会在局部域或全局域中寻找。我们要使用命名空间中定义的变量/函数,有以下三种方式:
指定命名空间访问(项目中推荐的方式)
使用域作用限定符::展开命名空间中的全部成员(项目不推荐,冲突风险较大)
使用using namespace指令展开命名空间,这样在使用命名空间时就可以不用在前面加上命名空间的名称。
using namespace namespace_name;#include<iostream> using namespace std; namespace space { int a = 0; int b = 1; } using namespace space; int main() { cout << a << endl; return 0; }- 展开命名空间中的某个成员(项目中经常访问的不存在冲突的成员使用)
使用using指令展开命名空间中的某个成员。
using namespace_name::namespace_member_name;#include<iostream> using namespace std; namespace space { int a = 0; int b = 1; } using space::a; int main() { cout << a << endl; cout << space::b << endl; return 0; }注意:
使用using展开时,在存在同名变量的情况下:
- 若存在局部域变量,则局部域变量的优先级最高;
- 若仅存在全局域变量与命名空间变量,会报错。
#include<iostream> using namespace std; int a = 10; namespace space { int a = 0; int b = 1; } using namespace space; int main() { // 存在局部变量 int a = 20; cout << a << endl;//输出 20 return 0; }#include<iostream> using namespace std; int a = 10; namespace space { int a = 0; int b = 1; } using namespace space; int main() { cout << a << endl; //编译报错: “a”: 不明确的符号 return 0; }2. 输入 & 输出
iostream是 Input Output Stream 的缩写,是标准的输入、输出流库,定义了标准的输入、输出对象。
注意:
C++ 中,没有包含<stdio.h>,也可以使⽤printf和scanf,在<iostream>中间接包含了。vs系列编译器是这样的,其他编译器可能会报错。
在 C++ 中,标准库都放在名为std的命名空间中。
std::cin是istream类的全局对象,它主要面向窄字符(char类型的变量)的标准输入流。std::cout是ostream类的全局对象,它主要面向窄字符的标准输出流。std::endl是一个函数,在流插入输出时,相当于插入一个换行符并刷新缓冲区。<<是流插入运算符,>>是流提取运算符号。
C++ 的输入输出方式相较于 C 语言的scanf和printf,C++ 的输入输出可以自动识别变量类型(本质通过函数重载实现),并且能够更好的支持自定义类型对象的输入和输出。
这是因为在 C++ 中,已经完成了内置类型输入输出的实现,并且支持对自定义类型的运算符重载。
#include<iostream> using namespace std; int main() { int a = 0; double b = 0.1; char c = 'x'; cin >> a >> b >> c; cout << a << " " << b << " " << c << endl; return 0; }#include<iostream> using namespace std; int main() { // 在io需求⽐较⾼的地⽅,如部分⼤量输⼊的竞赛题中,加上以下3⾏代码 // 可以提⾼C++IO效率 ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); }3. 缺省参数
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。
在调用该函数时,如果没有指定实参,则采用该形参的缺省值,否则使用指定的实参。
缺省参数分为全缺省和半缺省。(有些地方把缺省参数也叫默认参数)
- 全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。C++ 规定,半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
- 带缺省参数的函数调用,C++ 规定必须从左到右依次给实参,不能跳跃给实参。
- 函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须在函数声明中给缺省值。
编译器在看到函数调用的时候,只会查看函数声明,不会去找函数定义。默认参数实际上不是函数的一部分,而是编译器在调用时帮你补上。
#include<iostream> using namespace std; void Func1(int a = 10, int b = 20, int c = 30) { cout << "a = " << a << endl; cout << "b = " << b << endl; cout << "c = " << c << endl; } void Func2(int a, int b = 20, int c = 30) { cout << "a = " << a << endl; cout << "b = " << b << endl; cout << "c = " << c << endl; } int main() { Func1(); Func1(1); Func1(1, 2); Func1(1, 2, 3); //不支持Func1(,,3) Func2(1); Func2(1, 2); Func2(1, 2, 3); }例如在栈的实现中,我们便可以通过使用缺省参数来简化栈的初始化。
Stack.h: #include<iostream> #include<assert.h> typedef int STDataType; typedef struct Stack { STDataType* arr; int top; int capacity; }ST; void STInit(ST* ps, int n = 4); //////////////////////////////////////////////////////////////////////// Stack.cpp: #include<Stack.h> // 缺省参数不能声明和定义同时给 void STInit(ST* ps, int n) { assert(ps); STDataType* tmp = (STDataType*) malloc(n * sizeof(STDataType)); if (tmp == NULL) { perror("malloc"); exit(1); } ps->arr = tmp; ps->top = 0; ps->capacity = n; } //////////////////////////////////////////////////////////////////////// test.cpp #include"Stack.h" int main() { // 不知道要开多少空间 ST S1; STInit(&s1); // 确定要插入1000个数据,初始化时直接开好,避免后续扩容,提升代码运行效率 ST S2; STInit(&s2, 1000); }4. 函数重载
C 语言 是不支持同一作用域中出现同名函数的。
C++ 支持在同一作用域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同、类型不同或者参数类型顺序不同(本质还是类型不同)。
// 1、参数类型不同 int Add(int left, int right) { cout << "int Add(int left, int right)" << endl; return left + right; } double Add(double left, double right) { cout << "double Add(double left, double right)" << endl; return left + right; } // 2、参数个数不同 void f() { cout << "f()" << endl; } void f(int a) { cout << "f(int a)" << endl; } // 3、参数类型顺序不同 void f(int a, char b) { cout << "f(int a,char b)" << endl; } void f(char b, int a) { cout << "f(char b, int a)" << endl; }易错点:
- 返回值不同不能作为重载条件,因为调用时没法区分。
// 不够成重载 void fxx() {} int fxx() { return 0; }- 两个重载函数的调用入口不能重叠
// 下面两个函数构成重载,但是调用时,会报错 // 在不输入参数的情况下,存在歧义,编译器不知道调用谁 void f1() { cout << "f()" << endl; } void f1(int a = 10) { cout << "f(int a = 10)" << endl; }// 在输入参数的情况下,存在歧义,编译器不知道调用谁 void f1(int a = 10) { cout << "f(int a = 10)" << endl; } void f1(int a) { cout << "f(int a)" << endl; }5. 引用
5.1. 概念与定义
引用是给已存在的变量取一个别名,编译器不会为引用变量开辟内存空间, 它和它引用的变量共用同一块内存空间。
类型 & 引用别名=引用对象;#include<iostream> using namespace std; int main() { int a = 0; int& b = a; int& c = b;//c也相当于是a的引用 c++; //a,b,c的值相同 cout << a << " " << b << " " << c << endl; //a,b,c的地址相同 cout << &a << endl; cout << &b << endl; cout << &c << endl; return 0; }5.2. 引用的特性
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用不能改变指向,一旦引用一个实体,就不能再引用其他实体
#include<iostream> using namespace std; int main() { int a = 10; // 编译报错:引用 变量 "ra" 需要初始值设定项 // int& ra; int& b = a; int c = 20; // 并非让b引用c,引用不能改变指向 // 是一个赋值操作 b = c; cout << a << " " << b << " " << c << endl; cout << &a << endl; cout << &b << endl; cout << &c << endl; }5.3. 引用的使用
引用在实践中主要用于引用传参和引用做返回值,使得减少拷贝提高效率和改变引用对象的同时改变被引用对象。
- 引用传参和指针传参的功能是类似的,且引用传参相对更加方便。
// 函数重载 + 引用,简化程序,使用更加便捷 void Swap(int& x, int& y) { int tmp = x; x = y; y = tmp; } void Swap(double& x, double& y) { double tmp = x; x = y; y = tmp; }- 在部分 C 语言实现的数据结构中,可以使用 C++ 引用代替指针传参,在需要使用二级指针等复杂的指针时,可以简化程序。
#include<iostream> using namespace std; typedef int SLTDataType; typedef struct ListNode { SLTDataType val; ListNode* next; }LTNode, *PNode; // 不用二级指针,可以简化程序 // void LTPushBack(LTNode** phead, SLTDataType x) // void LTPushBack(LTNode*& phead, SLTDataType x) void LTPushBack(PNode& phead, SLTDataType x) { PNode newnode = (PNode)malloc(sizeof(LTNode)); newnode->val = x; newnode->next = NULL; if (phead == NULL) { phead = newnode; } else { //... } } int main() { PNode plist = NULL; LTPushBack(plist, 1); return 0; }- 引用作返回值,返回的是返回对象的别名(引用),能够对返回对象进行修改,也可以减少拷贝,提高效率。
传值返回会调用拷贝构造产生一个临时对象(右值)作为返回值,具有常性。因为大部分情况下,函数返回的是一个局部变量。
#include<iostream> #include<assert.h> using namespace std; typedef int STDataType; typedef struct Stack { STDataType* arr; int top; int capacity; }ST; void STInit(ST& rs, int n = 4) { rs.arr = (STDataType*)malloc(sizeof(STDataType) * n); rs.top = 0; rs.capacity = n; } void STPush(ST& rs, STDataType x) { if (rs.top == rs.capacity) { int newcapacity = 2 * rs.capacity; STDataType* tmp = (STDataType*)realloc(rs.arr, newcapacity * sizeof(STDataType)); if (tmp == NULL) { perror("realloc"); exit(1); } rs.arr = tmp; rs.capacity = newcapacity; } rs.arr[rs.top++] = x; } // int STTop(ST& rs) // 编译报错:“+=” :左操作数必须为左值 // 返回值为拷贝的临时变量,是一个右值,具有常性 // 返回的是引用,是一个左值,能够修改返回对象 int& STTop(ST& rs) { assert(rs.top > 0); return rs.arr[rs.top - 1]; } int main() { ST st; STInit(st); STPush(st, 1); STPush(st, 2); STTop(st) += 10; cout << STTop(st) << endl; return 0; }注意:不是所有的函数都可以将引用作为返回值。
传引用返回是有风险的。
如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用。野引用会导致很多奇怪的报错,且不同的编译器的报错可能不同。
传引用返回一定要确保返回对象,在当前函数结束后还在,才能用引用返回。
思考:
缺省参数,函数重载,引用,以及后面的运算符重载的意义是什么?
#include<iostream> #include<assert.h> using namespace std; #define N 100 typedef int SLTDataType; struct SeqList { SLTDataType arr[N]; int size; void Init() { size = 0; } void PushBack(int x) { assert(size < N); arr[size++] = x; } // 返回指定位置的值 int& AT(int i) { assert(i < size); return arr[i]; } int& operator[](int i) { assert(i < size); return arr[i]; } }; int main() { SeqList s; s.Init(); s.PushBack(1); s.PushBack(2); s.PushBack(3); s.PushBack(4); // 想让偶数位置的值 * 2 // 写法1 for (int i = 0; i < s.size; i++) { if (i % 2 == 0) { s.AT(i) *= 2; } } for (int i = 0; i < s.size; i++) { cout << s.AT(i) << endl; } // 写法2 for (int i = 0; i < s.size; i++) { if (i % 2 == 0) { s[i] *= 2; } } for (int i = 0; i < s.size; i++) { // cout << s.operator[](i) << endl; // 编译器转化为下面的函数调用 cout << s[i] << endl; } return 0; }显然,写法2 的可读性更强。
通过引用和运算符重载,实现了让自定义的数据结构支持类似数组的访问方式。
使代码更加直观,提高了代码的可读性和易用性,让自定义类型的使用体验更加接近内置类型。
5.4.const引用
- 引用一个
const对象必须用const引用,但是const引用可以引用普通对象。这是因为对象的访问权限可以缩小,但不能变大。
#include<iostream> using namespace std; int main() { const int a = 10; const int& ra = a; // 权限不能放大 // 编译报错:“初始化” :无法从“const int”转换为“int& ” // int& ra = a; int b = 20; // 权限可以缩小 const int& rb = b; int c = a; // 这里是赋值操作 // 不涉及权限的放大和缩小 return 0; }- 临时对象就是编译器需要一个空间暂存值时临时创建的一个未命名的对象, C++ 中把这个未命名对象叫做临时对象。
- 函数传值返回,表达式运算和类型转换会产生临时对象。
- C++ 中规定,临时对象具有常性,因此使用普通引用将会触发权限放大,必须要使用常引用。
#include<iostream> using namespace std; int main() { int a = 10; const int& ra = 3 * a; //编译报错: “初始化” 无法从“int”转换为“int& ” //int& ra = 3 * a; double b = 12.34; const int& rb = b; //编译报错: “初始化” :无法从“double”转换为“int& ” //int& rb = 3 * b; return 0; }#include<iostream> using namespace std; typedef int type; void Test(const int& x) { // void Test(int& x) // 对不改变参数的引用,一般会加上 const // 如果不加上const,Test(a)会报错 // 编译报错:无法从“const int”转换为“int &” } int main() { const int a = 10; int b = 20; Test(a); Test(b); return 0; }5.5. 指针和引用
C++ 中指针和引用的功能有重叠性,但是各有自己的特点,互相不可替代。
- 语法概念上,引用是一个变量的取别名,不开辟新的空间,而指针是存储一个变量地址,需要开辟新的空间。
- 在汇编层面,引用是用指针实现的,没有引用,只有指针。引用和指针都需要开辟新的空间。
- 引用在初始化时引用一个对象后,就不能再引用其他对象;而指针可以在不断地改变指向对象。
sizeof中,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数。
6.inline
用inline修饰的函数叫做内联函数,编译时 C++ 编译器会在调用的地方展开内联函数,这样调用内联函数就不需要建立栈帧,可以提高效率。
C语言 中,实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不方便调试。
C++ 设计了inline目的就是替代 C 语言 的宏函数。 在 C++ 中,通常会使用inline代替 C语言 中的宏函数,使用const和enum代替 C语言 中的宏常量。
实现一个宏函数的常见错误: #define ADD(int a, int b) return a + b; #define ADD(a, b) a + b; #define ADD(a, b) (a + b) 正确写法: #define ADD(a, b) ((a) + (b)) 为什么不能加分号? 宏本质是文本替换。 避免宏展开后产生额外语句,影响语法结构。 #define ADD(a, b) ((a) + (b)); if (1) ADD(1, 2); else cout << "error"; 替换后: if (1) ADD(1, 2); ; // error: else without a previous if else cout << "error"; 为什么要加外面的括号? 保证宏整体作为一个表达式,避免运算符优先级问题。 #define ADD(a,b) (a) + (b) int x = ADD(1,2) * 3; 替换后: int x = (1) + (2) * 3; 为什么要加里面的括号? 保护传入的参数,避免运算符优先级问题。 #define ADD(a,b) (a + b) ADD(1 << 2, 3) 替换后: (1 << 2 + 3) -> 1 << (2 + 3)inline对于编译器而言只是一个建议,也就是说,加了inline,编译器也可以选择在调用的地方不展开。不同编译器关于inline什么情况展开的情况各不相同,因为 C++ 标准没有规定。一般而言,inline适用于频繁调用的短小函数。对于递归函数或代码多些的函数,加上inline,也会被编译器忽略。
inline不建议声明和定义分离到两个文件,分离会导致链接错误。因为inline被展开而不是调用,因此编译时内联函数不会进入符号表,就找不到内联函数的函数地址,链接时会出现报错。因次,内联函数一般直接定义在.h文件中。vs2022的debug版本下默认是不展开inline的,这样方便调试,debug版本想展开,需要设置一下 以下两个地方。
为什么
inline只是一个建议?
因此,为了避免程序员滥用内联,内联只是给编译器的一个建议。
7.nullptr
NULL实际是一个宏,在stddef.h中,可以看到以下代码:
#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void*)0) #endif #endif- C++ 中
NULL可能被定义为字面常量 0,或者在 C 中被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,不可避免的会遇到一些麻烦。 - C++11 中引入
nullptr,nullptr是⼀个特殊的关键字,nullptr是⼀种特殊类型的字面量,它可以转换成任意其他类型的指针类型。使用nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,而不能被转换为整数类型。
#include<iostream> using namespace std; void f(int x) { cout << "f(int x)" << endl; } void f(int* ptr) { cout << "f(int* ptr)" << endl; } int main() { f(0); f(NULL); // 本想通过f(NULL)调⽤指针版本的f(int*)函数 // 但是由于NULL被定义成0,调⽤了f(intx),因此与程序的初衷相悖。 f((int*)NULL); // 需要强制类型转换才能调用函数 // f((void*)NULL); // 编译报错: “f”: 2个重载中没有⼀个可以转换所有参数类型 f(nullptr); return 0; }