【C++】编程、基础知识
文章目录
为什么尽量不要使用using namespace std?
增加了名称冲突的可能性。在一些大的项目中很容易造成和其他的名字冲突,同时容易引起错误,小项目你可能感觉不出来,但这种习惯其实不好,而且也就失去了名字空间本身的意义了
程序在执行int main(int argc, char *argv[])时的内存结构,你了解吗?
- 参数的含义是程序在命令行下运行的时候,需要输入argc 个参数,每个参数是以char 类型输入的,依次存在数组里面,数组是 argv[],所有的参数为指针类型
- char * 指向的内存中,数组的中元素的个数为 argc 个,第一个参数为程序的名称。
main函数的参数(int argc,char *argv[])
main函数的返回值有什么值得考究之处吗?
程序运行过程入口点main函数,main()函数返回值类型必须是int,这样返回值才能传递给程序激活者(如操作系统)表示程序正常退出。
main(int args, char **argv) 参数的传递。参数的处理,一般会调用getopt()函数处理,但实践中,这仅仅是一部分,不会经常用到的技能点。
防卫式声明
- 防卫式声明的作用是:防止由于同一个头文件被包含多次,而导致了重复定义。
- #ifndef 依赖于宏定义名,当宏已经定义时,#endif之前的代码就会被忽略,但是这里需要注意宏命名重名的问题;
- #pragma once 只能保证同一个文件不会被编译多次,但是当两个不同的文件内容相同时,仍然会出错。而且这是微软提供的编译器命令,当代码需要跨平台时,需要使用宏定义方式。
//1. 宏定义
#ifndef __FILENAME__
#define __FILENAME__
//...
#endif
//2. 编译器指令
#pragma once
const限定符
- 修饰内置类型
- 修饰自定义对象
- const与数组
- const与引用
- const与指针(顶层const与底层const)
- 修饰函数参数
- 修饰函数返回值
- 修饰成员函数
1. const修饰内置类型
在定义const变量的时候,必须进行初始化。编译器在编译过程中,会把用到该变量的地方全部替换成相应的值。
默认情况下,const仅在当前文件中有效。若希望多个文件使用同一个const变量,可以使用extern来修改,在任何声明和定义处使用(当然只需要定义一次)。
const int buff_size = 32;
buff_size = 64; // 错误,编译不通过
2. const修饰自定义对象
- 常对象中的成员变量不可以被修改,且只能调用常成员函数
- C++ 还提供了一个关键字mutable(参考【C++深陷】之“mutable”),它可以允许我们修改常量对象中的成员变量
const MyClass obj; // 常对象
3. const与数组
- 必须初始化;不可以执行arr[0] = 1类似的代码。
- 参考【C++深陷】之“数组”,我们了解编译器通常把数组处理为指向首元素的响应类型的指针,这里也是,类似于const int *类型。
//必须初始化;不可以执行arr[0] = 1类似的代码(指向可改,值不可改)
const int arr[] = {2, 3};
4. const与引用
4.1 定义方法
可以把引用绑定到const对象上,我们称之为对常量的引用。
const int ci = 422;
const int &r_ci = ci; // 常量引用
r_ci = 500; // 错误
4.2 常量引用的初始化
C++的引用必须与其所引用的对象的类型严格一致。
但是在初始化常量引用的时候,可以用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。
//例如非常量对象、字面值、表达式。
int i = 42;
const int &r1 = i; // 正确,常量引用绑定在一个非const变量上
const int &r2 = 42; // 正确,常量引用绑定在字面值上
const int &r3 = r1 * 2; // 正确,常量引用绑定在表达式结果上
当一个常量引用被绑定到另外一种数据类型上的时候,编译器执行了如下的过程:
- 生成另一个临时量,用目标对象的值初始化临时量;
- 将常量引用绑定到上述的临时量上。
看如下代码解释:
double d = 3.14;
const int &ri = d;
// 会被编译器解释成如下代码
double d = 3.14;
const int temp = d; // 执行double -> int的隐式转换
const int &ri = temp; // 将常量引用ri绑定到临时量上
若变量类型和常量引用一样不需要执行转换,则没有生成临时量这一步。
double d = 3.14;
const double &rd = d; // 不需要生成临时量
C++规定,将左值引用绑定到临时量是非法的;但是却可以将常量引用绑定在临时量上。因此,我们可以将函数的返回值声明成常量引用:
const int& get_a() {
int a = 3;
return a;
}
调用get_a将得到数值3。这一点也在【C++深陷】之“引用”中初始化引用的例外情况1提及:可以用临时量初始化常量引用。
这里其实可以用C++11的右值引用来解释。我们可以写成:(初始化右值引用必须是右值)
double d = 3.14;
const int temp = d; // 执行double -> int的隐式转换
const int &&ri = d; //合法(temp临时对象作为右值)
double d = 3.14;
const double &&rd = d;
// 编译不通过
// 初始化右值引用的必须是一个右值
5. const与指针
5.1 常量指针
和对常量引用一样,我们可以定义一个指向常量的指针:
const double pi = 3.14;
const double *ptr_pi = π
// 错误调用,不可以通过ptr_pi修改pi
*ptr_pi = 3.1415926;
- 指针的类型必须与其所指向的类型严格一致。
- 但是允许另一个指向常量的指针指向一个非常量对象。这种用法和常量引用类似,但是没有临时量的概念加入,基本数据类型必须是严格匹配的。
double pi = 3.14;
const double *ptr_pi = π
//这种用法,和常量引用类似,但是没有临时量的概念加入,基本数据类型必须是严格匹配的。
5.2 指针常量
因为指针是C++中的对象,引用仅仅是别名,因此不可以将引用设置为const,但是可以将一个指针声明为const。
实现的效果就是,该指针一旦初始化完成之后,就不能更改指针内容(不能修改地址),即不允许将该指针指向别的对象。
int buff_size = 512;
// pti将始终指向buff_size
int *const pti = &buff_size;
// 允许通过指针常量修改原对象的值
// 因为原对象也是非常量
*pti = 1024;
const double pi = 3.14;
// pt_pi是一个常量的指针常量
const double *const pt_pi = π
5.3 顶层const与底层const
- 使用术语顶层const,表示指针本身是个常量;
- 使用术语底层const,表示指向的对象是个常量。
更一般的,任意对象的const,都是顶层const
int i = 0;
// 顶层const,修饰普通对象
// 不允许改变ci的值
const int ci = 5;
// 其实可以写成 int const ci = 5;
// 顶层const,修饰指针对象
// 不允许改变cp_i的值
int *const cp_i = &i;
// 底层const,修饰指针指向的对象
// 不允许通过p_ci改变ci的值,即*p_ci = 10
const int *p_ci = &ci;
// 左侧的是底层const,右侧的是顶层const
// 既不允许改变cp_ci的值,也不能使用*cp_ci的改变ci的值
const int *const cp_ci = &ci;
// 底层const,引用都是底层const
// 不允许通过r_ci改变ci的值
const int &r_ci = ci;
注意:只有两处有底层const,一个是指针,一个是引用。
当执行对象的拷贝操作时,常量是顶层const还是底层const区别很大。
顶层const不受什么影响,对拷贝源不做过多要求,因为拷贝只关心拷贝源的值。
// ci是顶层const的常量
// 可以赋值给变量i
i = ci;
// cp_ci有顶层const
// 可以赋值给没有顶层const的p_ci
p_ci = cp_ci;
可以把非底层const的变量赋值给一个底层const
int a = 20;
int *p_a = &a;
// 非底层const赋值给底层const
const int *p_ca = p_a;
但是不可以使用底层const赋值给非底层const
int a = 20;
const int *p_ca = &a;
// 编译出错
// 底层const赋值给非底层const
int *p_a = p_ca;
6. const修饰函数参数
利用传递给函数的实参对函数形参初始化。形参的初始化方式和变量的初始化方式是一样的,因此调用函数传递const实参的规则,和上述的顶层const和底层const的规则一模一样。
因为C++支持函数的重载,又因为顶层const的要求很松,若如下声明了两个函数:
void func(const int i);
void func(int i); // 错误
当我们调用的时候区分不开,因此上述声明重复定义了func,是不允许的,这一点在【C++深陷】之“函数重载”也提及,在【C++深陷】之“函数匹配”中详细介绍了上述示例错误的原因。
C++标准建议,当我们不需要修改某个函数参数的值的时候,尽量将其声明为常量引用,即:
void func(const int &i);
7. const修饰函数返回值
函数返回一个值的方式和初始化一个变量或者形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
- 通常情况下,返回基本内置类型,不需要添加const,因为函数返回的是一个右值。
- 返回指针和引用的时候,如果不添加const,我们可能会写出如下代码:
string &shorter_string(string &s1, string &s2) {
return s1.size() <= s2.size() ? s1 : s2;
}
shorter_string(s1, s2) = "new string"; //虽然正确,但是很迷惑,通常添加const。
const string &shorter_string(const string &s1, const string &s2) {
return s1.size() <= s2.size() ? s1 : s2;
}
在第4节我们也提及了返回常量引用的函数,可以返回函数体内的临时量(函数内的局部变量):
// 调用get_a会得到数值3
const int& get_a() {
int a = 3;
return a;
}
8. const修饰成员函数
可以如以下方式定义一个const成员函数(常量成员函数):
class MyClass {
private:
int i = 0;
public:
int func() const; //常函数
};
// 既包含顶层const,又包含底层const
const MyClass *const this;
默认情况下,this的类型是指向类对象的指针常量:
// 这是一个顶层const,this指针的值是不允许被修改的
// 即只能指向本类的一个实例自己
MyClass *const this;
紧随参数列表之后的const关键字,其作用是修改隐式指针常量this的类型。
// 既包含顶层const,又包含底层const
const MyClass *const this;
关于函数的调用,有以下规则:
- 若my_class不是常量,且类只有func的const版本,则会调用func的const版本;
- 若my_class不是常量,同时声明了func的非const版本和const版本,则会调用func的非const版本。
class MyClass {
public:
int func() const { return 1; }
};
int main() {
// 不是常量
MyClass mc;
// 输出1
std::cout << mc.func() << std::endl;
}
class MyClass {
public:
int func() { return 0; }
int func() const { return 1; }
};
int main() {
// 不是常量
MyClass mc;
// 调用普通版本,输出0
std::cout << mc.func() << std::endl;
// 是常量
const MyClass c_mc;
// 输出1
std::cout << c_mc.func() << std::endl;
}
9. 总结
- C++中的const的是将对象和函数声明为只读状态的一个关键字。
- const可以与基本内置类型使用,此时定义的变量初始化之后就不会改变,编译器会将用到该变量的地方都替换成变量的值。
- const可以修饰自定义对象,此时只能调用const成员函数。(常对象只能调用常函数)
- const可以与引用使用,表明这是一个对常量的引用,通常称作常量引用。
- const可以与指针使用,要区分指向指针常量和常量指针,以及顶层const和底层const的定义。
- const可以修饰函数的参数,通常传递常量引用时使用。
- const可以修饰函数的返回值,通常在返回指针和引用时使用。
- const可以修饰成员函数,修改隐式指针常量this的类型,使常量对象可以调用成员函数。
类型别名(typedef)
//可以使用typedef关键字
typedef unsigned long idx;
//也可以使用别名声明(alias declaration) 技术,这是C++11新标准规定的方法:
using idx = unsigned long;
复合类型的类型别名
typedef double wages; // 用一个double存储工资 wages
using wages = double; // 同上
class MassachusettsInstituteOfTechnology;
typedef MassachusettsInstituteOfTechnology MIT;
using MIT = MassachusettsInstituteOfTechnology;
1 数组
// 可以表示二维网格的位置
typedef unsigned int ipos[2];
using ipos = unsigned int[2];
能看到typedef的定义方式,理解起来有些不同,但是using声明使用=赋值运算符,看起来直观一些。
理解typedef,我们首先考虑去掉typedef:
unsigned int ipos[2];
这时声明了一个数组变量,名字叫做ipos。这时我们加上typedef,告诉编译器这句话不是声明变量,而是声明类型别名。
// 告诉编译器
// 我声明的不是变量,是类型别名
typedef unsigned int ipos[2];
此时编译器知道,ipos表示的就是包含2个元素类型为unsigned int的数组。
ipos pos = {3, 4};
cout << pos[0] << ", " << pos[1]; // 输出3,4
也可以在声明类型别名的时候,不指定数组的长度:
// 可以表示二维网格的位置
// 也可以是三维、四维...
typedef unsigned int ipos[];
using ipos = unsigned int[];
但是在使用ipos时,必须初始化,告诉编译器数组的长度,这是数组的要求:
ipos pos = {2, 3, 4}; // 此时是三维网格的坐标
很明显使用类型别名更方便阅读
ipos pos = {3, 4};
ipos *p_ipos = &pos;
// unsigned int ipos[2] = {3, 4};
// unsigned int (*p_ipos)[2] = &ipos;
2 引用
typedef MyClass &rmc;
// using rmc = MyClass &
MyClass my_class;
rmc mc = my_class;
此时注意,我们似乎可以定义引用的引用了。
rmc &ref_mc = mc;
请读者注意,某些编译器上述代码是可以编译通过的。C++标准虽然规定了不可以定义引用的引用,但是编译器有自己的实现。而且上述代码,是通过一种间接的方式定义了引用的引用,从字面角度看,依旧是某种类型的引用,这个类型是程序员自己声明的类型,所以也不算是引用的引用。
3 指针
typedef char *pstring;
// using pstring = char *;
pstring ps = "hello world";
通常使用类型别名声明函数指针(前方高能):
typedef string name;
typedef bool (*pCompare)(const name &, const name &);
// using pCompare = bool (*)(const name &, const name &);
bool CompareLength(const name &s1, const name &s2) {
return s1.size() < s2.size();
}
bool CompareAlpha(const name &s1, const name &s2) {
return s1[0] < s2[0];
}
int main() {
const string s1 = "Matt", s2 = "Jimmy";
vector<pCompare> compares = {CompareLength, CompareAlpha};
bool flag = false;
for (const pCompare &pcom : compares) {
flag = flag || pcom(s1, s2);
}
cout << (flag ? "true" : "false") << endl;
return 0;
}
首先,将string取别名name,表示这个数据类型用来表示名字。
然后声明了一个函数指针pCompare类型,表示那些比较两个const name &,并返回bool类型的函数。
接下来定义的CompareLength和CompareAlpha就是这样的函数。
在main函数中,我们定义了一个类型为vector的compares容器,容纳所有比较操作。
遍历容器中的所有操作,将结果逻辑或||在flag上,输出flag。
这里有一个地方,在范围for循环中:
const pCompare &pcom : compares
仔细推敲一下pcom的类型,它是一个函数指针的常量引用。
若我们不使用typedef,该如何定义呢?
bool (* const &rCom)(const name &, const name &) = &CompareAlpha;
理解上述声明,要使用机器思维:从内向外,从右向左。
从标识符rCom开始。
- ①rCom向右是右括号,向左;看到&,rCom是引用类型。什么样的引用?
- ②向左看到const,常量引用;向左看到*,指针的常量引用。什么样的指针?
- ③看到左括号,向外;看到函数相关的定义,函数指针的常量引用。什么样的函数?
- ④接下来观察一下函数的定义,就知道是指向什么样的函数的指针了。
- 因为是引用,所以必须初始化。
2.4 const限定符
使用const限定符修饰类型别名,容易出错的地方就是对指针的理解。
- const + 引用,只有常量引用(实际上是指向常量的引用)一种情况。
- const + 指针,有顶层const和底层const之分。
typedef string *pname;
const pname n1 = new string("Matt"); //顶层const
请问上例const是顶层cosnt还是底层const?
答案:顶层const,即我们不能改变n1的值。
若把原类型带入,很可能得到:
const string *n1 = new string("Matt");
// 这是一个底层const
这是错误的理解。
实际上,pname已经是一个独立的类型了,const修饰的目标是对给定的类型的修饰,就是表示n1不允许被改变。
但是:
const pname *p_n1 = &n1;
// 这是一个底层const
2.5 总结
- 可以使用类型别名(type alias) 技术,即给某一个类型起一个别名,让复杂的名字变简单,易于理解和使用,帮助程序员清楚地知道使用该类型的目的。
- 使用typedef关键字或者使用C++11引入的别名声明都可以声明类型别名。
- 类型别名与复合数据类型在一起使用时,很容易出错。
宏定义(#define)
#define 宏名 替换文本
在预处理工作过程中,代码中所有出现的“宏名”,都会被“替换文本”替换。这个替换的过程被称为“宏代换”或“宏展开”
#define M 5 // 宏定义
#define PI 3.14 //宏定义
int a[M]; // 会被替换为: int a[5];
int b = M; // 会被替换为: int b = 5;
printf("PI = %.2f\n", PI); // 输出结果为: PI = 3.14
如果要写宏不止一行,则在结尾加反斜线符号使得多行能连接上,如:
#define HELLO "hello the wo\
rld"
printf("HELLO is %s\n", HELLO);
//输出结果为: HELLO is hello the wo rld
1、#define 与 typedef(类型别名) 的区别:
- 两者都可以用来表示数据类型
// 两者是等效的
#define INT1 int
typedef int INT2;
#define INT1 int *
typedef int * INT2;
INT1 a1, b1;
INT2 a2, b2;
b1 = &m; //报错
b2 = &n; // OK
所以两者区别在于,宏定义只是简单的字符串代换,在预处理阶段完成。而typede不是简单的字符串代换,而是可以用来做类型说明符的重命名的,类型的别名可以具有类型定义说明的功能,在编译阶段完成的。
2、有参宏(与函数的区别)
和函数类似,在宏定义中的参数成为形式参数,在宏调用中的参数成为实际参数。
#define N 5 //无参宏
#define COUNT(M) M * M //有参宏
printf("COUNT = %d\n", COUNT(10)); // 替换为: COUNT(10) = 10 * 10
// 输出结果: COUNT = 100
所以在宏定义中,有参宏的形参不分配内存单元,所以不作类型定义。而函数 中形参是局部变量,会在栈区分配内存单元,所以要作类型定义,而且实参与形参之间是“值传递”。而宏只是符号代换,不存在值传递。
类型转换
1. 隐式类型转换
隐式类型转换(implicit conversion)
是指无须程序员的介入、甚至不需要程序员了解的一种转换。
int ival = 3.541 + 3;
// 先将3转换成double,然后执行加法得到double类型临时量6.541
// 再将6.541小数部分忽略,得到6,初始化变量ival
编译器只会警告该运算损失了精度。
发生条件
在以下五种情况中,编译器自动执行隐式类型转换:
- 大多数表达式中,比int类型小的整形值提升为较大的整数类型(整型提升)
- 在条件语句中,非布尔值转换成布尔值(布尔转换)
- 在初始化过程中,初始值转换成变量的类型;赋值语句中,右侧运算对象转换成左侧运算对象的类型(赋值转换)
- 算术运算或关系运算的运算对象有多种类型,需要转换成同一种类型(严格匹配)
- 函数调用
1.1 整型提升(类型提升)
算术转换遵循的一个原则,整型提升:小整型提升到较大整型(发生条件1)。
因为C++标准只规定了各个数据类型的最小尺寸以及几种类型的大小关系,因此提升的结果根据编译器不同。
- 原则1:小整形bool、char、signed char、unsigned char、short、unsigned short将被提升成int。若int也装不下,就提升为unsigned int。
- 原则2:较大的char类型wchar_t、char16_t、char32_t提升成int、unsigned int、long unsigned long、long long和unsigned long long中最小的。
看下表:(假设int在某编译器上为32bit)
类型 | 含义 | 尺寸 | 提升 |
---|---|---|---|
bool | 布尔类型 | 8位 | int |
char | 字符 | 8位 | int |
wchar_t | 宽字符 | 32位 | int |
char16_t | Unicode字符 | 16位 | int |
char32_t | Unicode字符 | 32位 | int |
short | 短整型 | 16位 | int |
int | 整型 | 32位 | long |
long | 长整型 | 64位 | long long |
long long | 长整型 | 64位 | 无 |
对于浮点型,也存在类似的提升,float提升到double,double提升至long double。
虽然名称叫做整型提升,但是从整型到浮点型的转换,我们也可以理解为从精度较低整型的提升到精度较高的浮点型,因此有时将这两种转换统称为类型提升。
注意:提升不能一步到底:
char c = 'a';
float f = 3.14;
c + f;
// c首先提升成int
// int再转换成float
2.C++四种强制类型转换
在C++语言中新增了四个关键字static_cast、const_cast、reinterpret_cast 和 dynamic_cast。新类型的强制转换可以提供更好的控制强制转换过程。
2.1.static_cast
具有明确定义的类型转换,除了底层const,都可以使用static_cast运算符。
int i, j;
double s = static_cast<double>(j) / i;
// 使用static_cast将整型j转换为double
// i隐式转换为double
// 二者除法,得到double类型的结果,初始化s
2.2.const_cast
static_cast不能改变底层const,可以使用const_cast。
int a = 20;
const int *p_ca = &a;
int *p_a = const_cast<int *>(p_ca);
可以把非底层const的变量赋值给一个底层const
int a = 20;
int *p_a = &a;
// 非底层const赋值给底层const
const int *p_ca = p_a;
但是不可以使用底层const赋值给非底层const
int a = 20;
const int *p_ca = &a;
// 编译出错
// 底层const赋值给非底层const
int *p_a = p_ca;
使用const_cast:
int a = 20;
const int *p_ca = &a;
// 强制转换
int *p_a = const_cast<int *>(p_ca);
2.3.reinterpret_cast
reinterpret_cast为运算对象的位模式提供较低层次上的重新解释。
2.4.dynamic_cast
dynamic_cast的职责主要在具有继承关系的父类与子类之间。
1. dynamic_cast<type *>(expr) // 指针
2. dynamic_cast<type &>(expr) // 左值引用
3. dynamic_cast<type &&>(expr) // 右值引用
3种形式中所有的 type 必须是类类型且应该 含有虚函数。
3种形式对应的硬性要求分别是:
- 指针必须有效
- expr必须是左值
- expr不能是左值
满足任一以下条件,转换成功:
- expr是type类型的公有派生类
- expr是type类型的公有基类
- expr是type类型本身
否则转换失败。失败时:
- 指针返回0
- 引用抛出bad_cast异常
使用dynamic_cast最好进行if判断和异常获取,如下面的指针和引用的例子。
命名空间特点
- 命名空间既可以定义在全局作用域内,也可以定义在其它命名空间中,但是不能定义在函数或类的内部。
- 命名空间的名字必须在定义它的作用域内保持唯一;
- 命名空间作用域后面无须分号,即命名空间定义时最后面的花括号后面无需分号。
- 命名空间可以不连续。即,可以在多个文件中定义同一个命名空间,需要保证所有成员的名字不同;
- 命名空间可以嵌套定义。即命名空间内部可以定义另一个命名空间。
//定义一个名称空间F,并在里面添加两个函数start stop
namespace F {
void start(){}
void stop(){}
}
namespace G {
void start(){}
void stop(){}
}
//我们也可以在std名称空间中添加类,虽然一般不这么做,这里仅仅用于帮助我们理解什么是名称空间
namespace std {
struct Student{};
}
int main()
{
F::start();//通过名称空间名称F访问该名称空间中的函数start,下同
F::stop();
G::start();
G::stop();
std::Student my;
using namespace F;//将名称空间F中的名称暴露出来,不需要使用F::也可以访问
start();//再次执行名称空间F中的start函数
return 0;
}
名称空间std
std表示standard template library。C++ 标准模板库。C++为了方面我们开发会提供很多对象,函数放在其中给我们使用。cout 与 cin就在其中。
using
- 每次使用名称空间的对象都加上名称空间的名字,比如std::cout,的确也麻烦。
- 这时候可以使用using namespace std;来直接使用cout对象(程序会到std中找cout)
- 参考上面代码中的using namespace F;
相关文章
- 使用C++对物理网卡/虚拟网卡进行识别(包含内外网筛选)
- C/C++unlink函数的使用
- C#与C++之间类型的对应
- 【C++】基础知识
- C++程序设计三周教学记录
- C++程序设计:原理与实践(进阶篇)16.2 最简单的算法f?ind()
- 《C++程序设计教程(第3版)》——第1章,第3节程序设计方法
- C++ 基础知识
- Google C++ Style Guide在C++11普及后的变化
- 以前写的关于Linux C/C++的博客
- 《C++ 开发从入门到精通》——第2章 C++的重要特质分析2.1 什么是面向对象(object-oriented)
- 《C++ AMP:用Visual C++加速大规模并行计算》——3.4 extent< N >
- [第十届蓝桥杯省赛C++B组]等差数列
- bazel编译报错:absl/base/policy_checks.h:79:2: error: #error "C++ versions less than C++14 are not supported."
- C++类模板的声明和定义为什么要放在同一个文件
- 72、【哈希表】leetcode——454. 四数相加 II(C++版本)
- 高级C++驱动开发/专家(40-70万)
- 【C/C++】值传递和址传递区别解析