基础运用
约 10325 字大约 34 分钟
2025-06-21
1.C++关键字
C++ 比 C 语言多了很多关键字,如下表格:
| asm | do | if | return | try | continue |
| auto | double | inline | short | typedef | for |
| bool | dynamic_cast | int | signed | typeid | public |
| break | else | long | sizeof | typename | throw |
| case | enum | mutable | static | union | wchar_t |
| catch | explicit | namespace | static_cast | unsigned | default |
| char | export | new | struct | using | friend |
| class | extern | operator | switch | virtual | register |
| const | false | private | template | void | true |
| const_cast | float | protected | this | volatile | while |
| delete | goto | reinterpret_cast |
2.命名空间
2.1.命名空间概念
在 C++ 中,变量、函数、类是大量存在的,这些名字都会存储在全局作用域中,因此在使用的时候可能导致很多的冲突。
这种情况在多人协作的情况下尤其突出,多人份提交的代码很容易出现命名冲突的问题。
因此出现了针对命名重复的方案:命名空间。
使用命名空间可以对标识符的名字进行本地化,避免造成命名冲突或名字污染,
namespace就是针对这个问题的的关键字,而C语言没有办法解决这个问题(例如将库函数名字作为新定义的变量,但是这在C++中可以)C++允许同一个工程存在多个同名称的命名空间,编译器最后会合并成同一个命名空间里C++为了防止名字的冲突,便把标准库的东西都放入std这个命名空间。这样就可以使用标准库的名字来命名标准库的变量和函数。因此,要使用标准库的变量和函数也需要写出它的命名空间由于
C++采用命名空间,这个时候就需要注意新的坑了,在包含某些头文件的时候,有可能和自己定义的变量冲突,这个时候就需要使用好namespace关键字命名空间可以嵌套使用
2.2.创建命名空间
我们可以使用关键字 namespace 创建命名空间。
namespace limou3434//后面是这块命名空间的名字,前者 namespace 是命名空间的关键字
{
int print = 100;//在命名空间内定义一个变量
int function(int n)//在命名空间内定义一个函数
{
return n + 1;
}
struct Limou//在命名空间内定义一个结构体
{
int a;
char b;
float c;
double d;
};
namespace limou//在命名空间内嵌套一个命名空间
{
int e = 1;
int f = 2;
int g = 3;
}
}
int main()
{
return 0;
}2.3.命名空间合并
C++对于同名的命名空间不会认为是冲突,而是认为是同一个命名空间,会自动进行合并。
2.4.命名空间授权
有的人喜欢叫使用命名空间为“展开命名空间”,但是个人认为说成“授权”的方式可能会更好理解,即:命名空间通过 using 授权给我们使用它的内部成员名字的权限。
2.4.1.范围授权
2.4.1.1.小范围使用
使用作用域限定符 :: 单独引用,这样可以从命名空间中,单独拎出某个名字来使用,虽然可能有些繁琐,但是是最能解决命名冲突的方案。
命名空间名::名字;//命名空间名称:: 命名空间内的成员名字;
#include <stdio.h>
namespace limou3434//后面的 "limou3434" 是这块命名空间的名字
{
int print = 100;//在命名空间内定义一个变量
int function(int val)//在命名空间内定义一个函数
{
return val;
}
struct Limou//在命名空间内定义一个结构体
{
int a;
};
namespace limou//在命名空间内嵌套一个命名空间
{
int e = 1;
int f = 2;
int g = 3;
}
}
int main()
{
//1.使用变量
printf("%d\n", limou3434::print);
//2.使用函数
printf("%d\n", limou3434::function(9));
//3.使用结构体
limou3434::Limou A;
A.a = 100;
printf("%d\n", A.a);
//4.使用命名空间内嵌套的命名空间内的变量
printf("%d\n", limou3434::limou::e);
return 0;
}2.4.1.2.中范围使用
使用关键字 using 将命名空间的某个成员引入(项目里经常使用)。
using 命名空间名::名字;//using 其实就是英语单词“use”的变形
//using 命名空间名称:: 命名空间内成员名字;
#include <stdio.h>
namespace limou3434//后面是这块命名空间的名字
{
int print = 100;//在命名空间内定义一个变量
int val = 10;
int x = 1;
}
using limou3434::print;//单独拎出一个变量,之后可以一直使用
int main()
{
printf("%d\n", print);
print += 1;
printf("%d\n", print);
//printf("%d\n", val);//这个语句不可以使用,因为没有单独拎出 val 名字
using limou3434::val;
printf("%d\n", val);//这里就可以使用
return 0;
}2.4.1.3.大范围使用
使用关键字 using 和 namespace 将命名空间的整体引入。这样写可能不太好,会把所有标准库的名字全部暴露,有可能和自己写的名字冲突。
using namespace 命名空间名;#include <stdio.h>
namespace limou3434//后面是这块命名空间的名字
{
int print = 100;//在命名空间内定义一个变量
int val = 10;
int x = 1;
}
using namespace limou3434;
using namespace std;//这里使用了标准命名空间,内部有标准库的各种变量、函数等名字,这里是为了使用 cout 这个名字,cout 的功能是输出,类似 printf(),但是比 printf()更加智能,可以自动识别变量的类型进行输出
int main()
{
cout << print;
cout << val;
cout << x;
return 0;
}补充:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于命名空间中,只有使用
using才可以在命名空间外被使用。
2.4.4.嵌套避免
实际上 Cpp 的命名空间要比想象中的要复杂,待补充...
// 尝试嵌套命名空间
#include <iostream>
void foo() {
std::cout << "foo" << std::endl;
}
namespace A {
void foo() {
std::cout << "A::foo" << std::endl;
}
namespace B {
void foo() {
std::cout << "A::B::foo" << std::endl;
}
namespace C {
void foo() {
std::cout << "A::B::C::foo" << std::endl;
}
void callFoo() {
foo(); // 调用 A::B::C::foo()
C::foo(); // 调用 A::B::C::foo()
B::C::foo(); // 调用 A::B::C::foo()
A::B::C::foo(); // 调用 A::B::C::foo()
B::foo(); // 调用 B::C::foo()
A::B::foo(); // 调用 B::C::foo()
A::foo(); // 调用 A::foo()
::foo(); // 调用全局的 foo(), 这样就不会调用不到全局的函数了
}
}
}
}
int main() {
A::B::C::callFoo();
return 0;
}在标识符最前面加上 :: 可以让一个标识符被其他人在自己的
3.C++输入和输出
在 2.4.3.大范围 中我们使用了 std,接下来让我们来详细了解一下 C++ 特有的输入输出方式。
3.1.输入输出的使用
C++ 使用了更加灵活方便的 cin 和 cout 进行流输入和流输出。
#include<iostream>//必须包含这个头文件
using namespace std;//并且释放和 cin 和 cout 相关的命名空间
int main()
{
int a = 0;
cin >> a;
cout << a << endl;//endl 是换行的意思,类似'\n',实际上您也可以使用'\n'替代 endl 得到换行的效果
return 0;
}3.2.输入输出的解释
使用
cout和cin必须包含头文件<iostream>(注意新的C++标准头文件不再使用.h来表示一个头文件。早期标准库将所有功能都在全局域中实现,声明在.h的头文件中,使用的时候include一下就行,后来将这些实现改到std命名空间下。为了和头文件区分并且正确使用命名空间,规定C++头文件不带.h),两者的前缀c就是单词console即“控制台”的缩写。另外,有的老旧编译器还支持#include<isstream.h>的形式,而后续的编译器大部分都不再支持使用
cout和cin必须指出其命名空间std,就是标准命名空间<<是流插入运算符,>>是流提取运算符(也有叫“输入输出符号”的,统称“流运算符”)C++的输入输出可以自动识别变量的类型,无需指定输入输出格式,这里的底层逻辑等您学到“运算符重载”后就可以明白了虽然是自动控制,但是
cout和cin还有其他复杂用法,包括控制输出格式等等,但是这些对比后续学习内容不算重点,就先不在此描述(而且C++也依旧支持C的printf()和scanf()等库函数)std是C++标准库的命名空间,在日常练习中虽然可以直接将整个命名空间全部暴露,但是在大型工程中,一般都会使用作用域限定符,只暴露某一成员endl是“换行”的意思。也是需要包含头文件<iostream>,可以用转义字符\n替代
补充:
实际上
cin是istream类的对象,而cout是ostream类的对象.流运算符
<<和>>则是经过运算符重载后的运算符上面这部分内容等到后面学到“类”的时候再回来看看您就能明白了...
现在对您来说只需要知道这么使用即可!
4.缺省参数
4.1.缺省函数的概念
在 C++ 中声明或定义函数的时候可以为函数指定一个缺省值(默认值,这里是翻译问题,才叫“缺省”)。如果在使用函数的时候没有指定实参,就使用默认值为函数参数,否者使用指定的实参。
#include<iostream>
using namespace std;
void function(int a = 0)//这里的 a 就有一个缺省参数
{
if (a == 0)
{
cout << "你好!!!" << endl;
}
else
{
cout << "hello!!!" << endl;
}
}
int main()
{
function();
function();
function(1);
return 0;
}4.2.全半缺省的使用
全缺省:可以只指定一部分参数有默认值,或者全部指定有默认值
半缺省:半缺省函数的参数必须“从右往左”给出,不可间隔给予默认值,因此
C++没有允许(,,1)这样的写法缺省值不能在函数声明和定义里同时出现,要以声明为准(只写在声明中,当然如果函数声明和定义是同一份代码就没有这个问题)
缺省值必须是常量或者全局变量
//.h 文件
voide function(int a = 100);//.cpp 文件
void function(int a = 50)
{
//某些具体代码
}//在 VS2022 中哪怕重定义的缺省的值是一样的也不行
#include <iostream>
using namespace std;
void fun(int a = 0);
int main()
{
fun();//报错无法使用
return 0;
}
void fun(int a = 0)
{
if (a == 0)
{
printf("0\n");
}
else
{
printf("%d\n", a);
}
}5.函数重载
5.1.重载的概念
在现实生活中有的词语可以根据上下文语义,从而产生不同的意思,这实际上就是一种重载的体现。
在
C++中允许在同一个“作用域”中声明几个类似功能的同名函数。这些函数的形参列表(形参个数、形参类型、形参顺序)是不同的,常常用来处理实现功能类似、数据不同的问题。而且也根据这个形参的使用来区分不同重载的函数(注意不同命名空间内定义同一个名字的函数虽然可以,但是那个不叫“重载”)需要注意的是:“返回值的不同、形参名字的不同”都不构成函数重载
实际上
cout、cin能够自动识别的本质也是函数重载,在库里面已经帮我们实现好了
注意:返回值不计入重载
这一点很重要,返回的类型并不能构成重载,理由如下代码:
int add(int a, int b); double add(int a, int b);如果这里两个
add()构成了重载,那么在函数调用的时候(比如:add(1, 2))就会发生混乱,编译器不知道要调用哪一个函数,是返回int还是double。
5.2.重载的使用
#include<iostream>
using namespace std;
void function(int a = 0)
{
if (a == 0)
{
cout << "你好!!!" << endl;
}
else
{
cout << "hello!!!" << endl;
}
}
char function(char b)
{
cout << b << endl;
return 1;
}
int main()
{
function();
function(1);
function('c');
return 0;
}5.3.重载的原理
为什么重载在 C 语言不支持,而 C++ 语言却支持?(以下现象只有在汇编代码中才能看出来)
要明白这个过程首先需要重新复习
C++代码的编译链接过程: a.预处理:头文件展开、宏替换、条件编译、去掉注释 b.编译:语法检查、生成汇编代码 c.汇编:将汇编代码转化为二进制机器码,生成目标文件 d.链接:将程序合起来,生成可执行程序(这个合起来的意思就是将只给声明的函数地址找到链接起来,如果找不到就是链接错误。而具体的找法就是为每个目标文件做一个符号表,通过符号表来寻找对应的地址)而在
C语言就是在这个链接阶段,查找符号表的时候,有两个同名函数符号且地址不同,这就发生了冲突。即:“C语言直接将函数名作为符号来对应函数地址”而
C++语言在这个链接阶段时,函数符号是根据函数名和参数名来生成的,这样符号名字不一样,地址也不一样,就可以构成函数重载的用法。即:C++语言将函数名和参数类型信息等组合起来构成符号来对应函数地址,这也解释了为什么函数重载需要靠参数类型来标识不同的重载函数
综上所述,C 语言不支持函数重载(没有函数修饰规则),C++ 语言支持函数重载(有函数修饰规则)。
另外,在 C++ 这个符号表里的“函数修饰规则”在不同环境是有可能不一样的,但是一定是依赖函数参数来生成的
这种原理就是“名字修饰”,另外这里有一份 VS 的“C/C++调用约定博文”
补充:重载和缺省值的结合
如果给出下面代码会发生什么情况呢?
#include <iostream> using namespace std; void func(int a) { cout << "void func(int a)" << endl; } void func(int a, int b = 1) { cout << "void func(int a,int b)" << endl; } int main() { func(1, 2);//调用成功 func(1);//调用失败,重载语义模糊,编译器不知道该调用哪一个函数 return 0; }
6.引用
6.1.引用的概念
引用不是新定义一个变量,而是为已存变量取个别名,引用变量不会开辟新的内存空间,它和它引用的变量共用同一块内存空间。
6.2.引用的使用
类型& 引用变量名(对象名) = 引用实体;引用必须在定义的时候就必须进行初始化,
C++的引用一旦初始化就不能改变指向,这也是C++不能完全脱离指针的一个体现(Java是可以改变指向的)一个变量/对象可以有多个引用
引用一旦引用一个实体,就再也不能引用其他实体
“引用”是给变量取别名,
typedef是给类型取别名,两者有某些相似的地方还可以给引用后的别名取引用,也就是嵌套引用
引用更多是在函数的参数处使用,尤其是大对象传参的时候会提高效率
引用还可以在函数的返回值处使用,但是注意函数内部定义的变量一旦出函数作用域就会被销毁,此时不能使用引用返回,只能使用传值返回(但是如果这个局部变量被静态关键字
static修饰,那就可以直接使用引用返回)引用的变量类型和引用类型不一样时,会发生权限放大错误,本质是创建了临时变量,而临时变量具有常属性
//引用的基本使用
int a = 10;
int& b = a;
int x = 20;
b = x;//因此这个语句的意思是“x 的值赋给 b”,而不是“x 成为了 b 的别名”,这跟指针就有很大的区别const int a = 10;
//int& ra = a; //该语句编译时会出错,a 为常量
const int& ra = a;
//int& b = 10; //该语句编译时会出错,b 为常量
const int& b = 10;
double d = 12.34;
//int& rd = d; //该语句编译时会出错,由于类型不同所以 d 会先赋值给一个临时变量,该临时变量的类型是 const int,但是使用引用的时候类型不匹配(int 和 const int 不匹配),发生了权限放大
const int& rd = d;//此时就可以正常引用了6.2.1.多重引用
//引用后变量的地址
#include <stdio.h>
int main()
{
int a = 10;//实际变量
int& a1 = a;//引用 1
int& a2 = a;//引用 2
printf("%d %d %d\n", a, a1, a2);
printf("%p %p %p\n", &a, &a1, &a2);
return 0;
}6.2.2.做函数参数
//函数参数列表的引用
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int x = 0, y = 2;
Swap(x, y);
}6.2.3.做函数返回值
//函数返回值的引用
int& function(int& x)//int& x = i,因此 x 是 i 的别名
{
x++;//这个 x++等价于 i++
return x;//返回 x,int& ("function()") = x,因此函数甚至可以被赋值
}
int main()
{
int i = 0;
int j = 0;
j = function(i);
printf("%d\n", j);
printf("%d\n", function(i) = 10);//函数也可以赋值
return 0;
}//但是返回值引用绝对不能返回局部变量的引用
int& Count()
{
int n 0;
n++;
return n;//错误返回,因为 n 被销毁了
}
int main()
{
int& ret = Count();
cout << ret << endl;
return 0;
}//使用引用书写顺序表
typedef struct SeqList
{
int* a;
int size;
int capacity;
}SL;
//初始顺序表
void SLPushInit(SL& s, int capacity = 4){/*...*/}
//尾插顺序表
void SLPushBack(SL& S, int x){/*...*/}
//修改顺序表
int& SLAt(SL& s, int pos)
{
assert(pos >= 0 && pos <=s.size);
return s.a[pos];
}
int main()
{
SL sl;
SLPushInit(sl);//初始化
SLPushBack(sl, 1);//尾插
SLPushBack(sl, 2);//尾插
SLPushBack(sl, 3);//尾插
SLPushBack(sl, 4);//尾插
for(int i = 0; i < sl.size; ++i)
{
cout << SLAt(sl, i) << endl;//输出顺序表的元素
}
SLAt(sl, 0)++;//拿到 s.a [pos],对其进行++,这是只有 C++才有的操作,C 语言无法实现
SLAt(sl, 0) = 10;//拿到 s.a [pos],修改成 10
}6.2.4.常引用
//引用类型的不匹配情况
int main()
{
const int a = 10;
//int& a1 = a;//错误引用
const int& a1 = a;
//int& b1 = 10;//错误引用
const int& b1 = 10;
double c = 3.14159;
//int& c1 = c;//错误引用
const int& c1 = c;
return 0;
}//引用权限的变化
#include <iostream>
using namespace std;
int main()
{
const int c = 20;//c 可读不可写
//int& d = c;//d 把 c 权限放大了(可读可写),这是不被允许的
const int& d = c;//这是允许的,属于权限平移
int e = 30;
const int& f = e;//但是权限缩小是被允许的
int g = 1;
double h = g;//这里产生一个临时变量,将存储数据提升后的 g,再赋予(拷贝)h(这里 g 用显式强转也不行,无论是显式还是隐式,都不会改变 g 本身的类型)
//double& i = g;//这是不被允许的,因为这里产生一个临时变量,将存储数据提升后的 g,而这个临时变量具有常属性,临时变量被 h 引用的话发生了权限放大
const double& i = g;//这是被允许的,只是发生了权限平移
const double& j = 3.14;//这个也是被允许的,因此拥有 const 修饰的引用允许引用常量。所以如果是在函数形参处使用引用时,如果不需要改变值,就要尽可能使用 const 修饰
return 0;
}
//在 C/C++中规定,类型转换(隐式和显式)过程会产生临时变量,如果我们使用 const 引用这个临时变量,则会让编译器延长这个临时变量的使用时间(生命周期)注意:权限放大和缩小只适用在指针和引用上,普通变量之间的赋值是没有作权限要求的,因为这只是做一份临时拷贝。
#include <iostream>
using namespace std;
void function_1(int n)
{
;
}
void function_2(int& n)
{
;
}
void function_3(const int& n)
{
;
}
int main()
{
int a = 10;
const int b = 20;
function_1(a);
function_1(b);
function_1(30);
function_2(a);
//function_2(b);//这是不被允许的
//function_2(30);//这是不被允许的
function_3(a);
function_3(b);
function_3(30);
return 0;
}int fun()
{
int a = 0;
return a;
}
int main()
{
const int& x = fun();//这里返回的 a 的拷贝,这个拷贝由一个临时变量维护,为了能被引用,被编译器延长了其生命周期
return 0;
}6.3.引用和指针
指针更强大、更危险、更复杂/引用局限一些、更安全、更简单。
| 引用 | 指针 | |
|---|---|---|
| 概念 | 引用在概念上定义一个变量的别名 | 指针存储一个变量地址 |
| 空间 | 引用没有开空间 | 但是指针开了 4 个字节或是 8 个字节的空间(32 位或者 64 位) |
| 初始 | 引用在定义时必须初始化 | 指针被声明后可以不进行初始化 |
| 赋值 | 引用在初始化时引用一个实体后,就不能再引用其他实体 | 而指针可以在任何时候指向任何一个同类型实体 |
| 置空 | 没有 NULL 引用 | 但有 NULL 指针,但是 C++ 改为 nullptr 指针 |
| 大小 | 在 sizeof 中含义不同,引用结果为引用类型的大小 | 但指针始终是地址空间所占字节个数(32 位平台下占 4 个字节,64 位平台下占 8 个字节) |
| 运算 | 引用自加,即引用的实体增加 1(加的是类型的 1) | 指针自加,即指针向后偏移一个类型的大小 |
| 级别 | 没有多级引用 | 但是有多级指针 |
| 访问 | 访问实体方式不同,引用编译器自己处理 | 指针需要显式解引用 |
| 安全 | 引用比指针使用起来相对更安全 | 使用不当时,严重时会使代码奔溃 |
7.内联函数
7.1.内联的概念
内联关键字是
inline,被其修饰的函数就叫内联函数编译的时候,只要满足条件,
C++编译器会在调用内联函数地方直接展开,没有函数调用建立栈帧的开销,内联函数能提升程序运行的效率,因此这是一种空间换时间的做法内联关键字的使用很像宏的使用,可以说是宏的优化版本(但是宏:没有类型安全检查、容易出错、不易于调试)
内联可以嵌套内联函数使用
内联函数在
VS2022的debug模式下默认是不展开的(rlease),这里打开反汇编就会发现依旧是调用函数,这是为了方便调试设计的,这是可以通过设置规避(“配置属性”-“C/C++”-“常规”-“调试信息格式”-“程序数据库(/Zi)”并且修改“配置属性”-“C/C++”-“优化”-“内联函数拓展”-“只使适用于__inline(/Obl)”)来查看汇编代码(但是有的编译器不支持)。inline类似C语言里的寄存器变量关键字,只是向编译器发出一个请求,不同编译器关于inline的实现机制可能不一样,也就是说:内联申请不一定会成功!inline本质是一种空间换时间的做法。如果编译器将函数当成内联函数,在编译期间就会将函数体替换函数调用用,对应得优劣势有: a.劣势,有可能使得目标文件变大 b.优势,少了调用开销,提高程序的运行效率一般建议在以下情况来使用
inline:将函数规模小、非递归、非频繁调用的函数采用inline修饰,否则编译器有可能会忽略inline的特性(这是因为,函数内部代码指令如果比较长,有可能会让编译的程序暴增,导致编译产生的程序变大。这些更多取决于编译器对inline的实现和理解)另外被内联关键字修饰的函数,其声明和定义一般不建议分开写,分开有可能导致链接错误(尤其是不在一个翻译单元)。因此最好是把内联函数的声明和定义直接一起写到头文件里,不要去做分离(内联没有必要在符号表生成函数的地址,因此不会生成栈帧操作,链接的时候就会出现错误)!
补充:可以在同一个项目的不同源文件内定义函数名相同但实现不同的
inline函数,因为inline函数会在调用的地方展开,所以符号表中不会有inline函数的符号名,不存在链接冲突这一说法。
7.2.内联的使用
7.2.1.正确使用
#include<iostream>
using namespace std;
inline int add(int a = 0, int b = 0)
//被 inline 修饰的函数
{
return a + b;
}
int main()
{
int c = 0;
c = add(3, 5);
cout << c;
return 0;
}7.2.2.不在同一个翻译单元
下面看看一种错误的用法():
//inline.h 内部
inline int Add_Inline(int x, int y);//inline.cpp 内部
#include "inline.h"
int Add_Inline(int x, int y)
{
return x + y;
}//main.cpp 内部
#include <iostream>
#include "inline.h"
int main()
{
//无法调用内联函数 Add
std::cout << Add_Inline(10, 10) << std::endl;
return 0;
}//对 main.cpp 来说定义和声明不在一个翻译单元里,无法链接(因为两个源文件属于不同的编译单元,它们会分别进行编译,而不会互相影响,最后链接的时候又不会有地址提供链接。)7.2.3.共在同一个翻译单元
再看看修改后的代码:
//inline.h 内部
inline int Add_Inline(int x, int y);
int Add_No_Inline(int x, int y);//inline.cpp 内部
#include "inline.h"
int Add_Inline(int x, int y)
{
return x + y;
}
int Add_No_Inline(int x, int y)
{
return Add_Inline(x, y);
}//main.cpp 内部
#include <iostream>
#include "inline.h"
int main()
{
//成功调用内联函数 Add,对 inline.cpp 来说定义和声明在一个翻译单元里,
//内联函数是在一个翻译单元内被展开并且在内部被使用的,所以可以使用
std::cout << Add_No_Inline(10, 20) << std::endl;
return 0;
}7.4.内联和宏量
| 宏的优点 | 宏的缺点 |
|---|---|
| 提高代码的复用性(让你的代码能适应更多种的情况,完成更多种情况的任务,这就是代码的复用性)代码的可维护性变强,修改某些常量值快捷方便 | 不方便调试带有宏的代码(因为预编译阶段进行了替换,而调试是在编译后的) |
| 宏函数能提高效率,减少栈帧的建立 | 导致代码的可读性差、有一点的复杂性,容易误用 |
| 没有类型安全的检查(替换机制),易出现类型错误 |
宏的代替方案有如下两点:
常量定义可以使用
C++的const、enum代替宏常量短小函数定义可以使用
C++的内联关键字inline代替宏函数
7.6.内联的约定
在现代 C++ 中,基本建议尽量使用 const、enum、inline,而不使用宏。
补充:
const定义的常量只有一次拷贝(就是只有一个值)而
define定义的变量在内存中并没有拷贝,因为所有的预处理指令都在预处理时进行了替换
7.7.内联的技巧
使用内联可以有效减少大量的样板代码,例如自动设置 set&get。
#include <iostream>
#include <string>
// 通用属性类模板
template <typename T>
class Property {
public:
// 构造方法
Property() = default;
Property(const T& value) : value_(value) {}
// 禁用赋值操作符
Property& operator=(const Property&) = delete;
// 内联 getter 和 setter 方法
inline T get() const { return value_; }
inline void set(const T& value) { value_ = value; }
private:
T value_;
};
// 使用具体类型的 Person 类
class Person {
public:
// 构造函数
Person(const std::string& name, int age)
: name(name), age(age) {}
// 公有属性
Property<std::string> name;
Property<int> age;
};
int main() {
// 创建 Person 对象
Person person("Alice", 30);
// 设置属性值
person.name.set("Bob");
person.age.set(31);
// 获取属性值
std::cout << "Name: " << person.name.get() << ", Age: " << person.age.get() << '\n';
return 0;
}补充:
#define DECLARE_GETTER_SETTER(type, name) \ private: \ type name##_ ; \ public: \ type get_##name() const { return name##_; } \ void set_##name(type value) { name##_ = value; } class Person { DECLARE_GETTER_SETTER(std::string, name) DECLARE_GETTER_SETTER(int, age) };
8.typedef 和 auto
8.1.typedef 关键字
随着一个工程的扩大,程序中用到的类型也越来越复杂,经常体现在:
类型难以拼写,容易拼错
含义不明确,导致用错
在编程时,常常需要把表达式的值赋值给变量,这就要求在声明变量的时候就要清楚知道表达式的类型。要做到这点并非那么容易,而 typedef 也还不够智能,而且因此 C++11 给 auto 一个新的定义。
/*没有使用 tepedef 重命名*/
//这段代码现在看不懂没关系,您只需要知道类型的名字确实很长就对了
#include <string>
#include <map>
int main()
{
std::map<std::string, std::string>m{ { "apple", "苹果" }, { "orange", "橙子" }, {"pear", "梨"} };
std::map<std::string, std::string>::iterator it = m.begin();
//其中 std:: map <std::string,std::string>:: iterator 是一个类型,但是类型的名字太长了,容易写错,可以尝试使用 typedef 给这个类型取个别名
while (it != m.end())
{
//....
}
return 0;
}8.2.auto 关键字
在
C++11的标准中,auto不再是存储类型说明符,而是一个新的类型指示符,来指示编译器auto声明的变量必须由编译器在编译时期推导而得使用
auto定义变量时必须对其进行初始化,在编译阶段编译器会根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将 auto 替换为变量实际的类型。
如果结合指针和引用来使用(这实际上也是指针和引用的区别之一):
结合指针的话,
auto和auto*是没有区别的结合引用的话,
auto和auto&是有区别的,必须要加&
#include <iostream>
using namespace std;
int main()
{
//一个变量
int x = 100;
//1.结合指针
auto a = &x;
auto* b = &x;
//2.结合引用
auto c = x;
auto& d = x;
//测试类型和输出,typeid 可以打印类型
cout << typeid(a).name() << " " << a << endl;
cout << typeid(b).name() << " " << b << endl;
cout << typeid(c).name() << " " << c << endl;
cout << typeid(d).name() << " " << d << endl;
//3.修改变量
*a = 10;
cout << x << endl;
*b = 20;
cout << x<< endl;
c = 30;
cout << x << endl;
d = 40;
cout << x << endl;
return 0;
}当在同一行使用 auto 来声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量
#include <iostream> using namespace std;
int main()
{
//正确使用
auto a = 1, b = 2;
//错误使用
//auto c = 3, d = 4.0;//该行代码会编译失败,因为 c 和 d 的初始化表达式类型不同
return 0;
}auto 不能推导的场景有:
auto不能作为函数的形参,因为函数编译是要建立栈帧的,这个时候都不知道形参的大小,怎么知道从哪里开始创建栈帧呢?auto不能用来声明数组
另外,为了避免和 C++98 的 auto 发生混淆,C++11 只保留了 auto 作为类型指示符的用法。
auto 的最大的优势其实在于 C++11 提供的新式 for 循环以及 lambda 表达式,lambda 表达式我们先不提,但是下面我们来了解一下范围 for。
9.基于范围的 for 循环
这不是 C++ 的首创,而是借鉴其他语言引入进来的。
9.1.范围 for 的使用
- 在
C++98 之前,遍历一个数组可以按照以下的方式使用
#include <iostream>
using namespace std;
//使用 C++98 遍历方式
int main()
{
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循环。for后面的括号由冒号:分为两部分,第一部分是范围内用于迭代的变量,第二部分表示被迭代的范围
#include <iostream>
using namespace std;
//使用 C++11 遍历方式
int main()
{
int array[] = { 1, 2, 3, 4, 5 };
for(auto& e : array)
e *= 2;
for(auto e : array)
cout << e << " ";
return 0;
}9.2.范围 for 的条件
由于 C++ 不支持直接传数组(这样消耗大,浪费)所以在函数传数组的时候必须提供 begin 和 end 方法,begin 和 end 就是 for 循环迭代的范围(有关 begin 和 end 的具体使用后面在使用 string 类再说)
void function(int arr[])//这个函数是不正确的,因为 arr 不是数组名
{
for(auto& e : arr)//无法使用范围 for
cout << e << endl;
}10.指针空值 nullptr
10.1.NULL 的概念
//NULL 实际是一个宏,在传统的 C 头文件(stddef.h)中,可以看到如下代码
#ifndef NULL
#ifdef __cplusplus
#define NULL 0//在 C++语言中
#else
#define NULL ((void *)0)//在 C 语言中
#endif
#endif
//在 C++98 中,字面量 0 既可以是一个整型数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看作整型常量,如果这么使用 NULL 时,就会具有一定的麻烦,这是 C++设计的一个失误,都是 C++不敢去掉这个定义,为了保证兼容性就新定义了 nullptr。
void f(int)
{
cout<<"f(int)"<<endl;
}
void f(int*)//函数重载
{
cout<<"f(int*)"<<endl;
}
int main()
{
f(0);
f(NULL);//误用第一个函数,因为处理 NULL 的时候,NULL 是被定义为 0 的
f((int*)NULL);//需要使用强制类型转换才可以使用第二个函数
return 0;
}10.2.nullptr 的概念
而在使用
nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的 6在
C++11中,sizeof(nullptr)与sizeof((void*)0)所占的字节数相同为了提高代码的健壮性,在后续表示指针空值时建议最好使用
nullptr
11.decltype
decltype 可以根据表达式的类型来创建变量,某些时候可以使用 auto 替代,但有些情况不可以。
#include <iostream>
int main()
{
const int x = 1;
double y = 2.3;
//decltype 可以根据表达式的类型来创建变量
decltype(x * y) ret;//ret 的类型变成了 double
decltype(&x) p;//p 的类型变成了 const int*
std::cout << typeid(ret).name() << '\n';
std::cout << typeid(p).name() << '\n';
return 0;
}补充:该操作我以后也会提及,您可以暂时跳过这一部分。
12.原始字符串
有关 C++ 的原始字符串其实还是比较简单的,C++ 可以将字符串分为两种,一种是标准字符串,例如:"\"Hello, I' am limou3434.\"\n",而如果使用原始字符串则为 R"(\"Hello, I'am limou3434.\"\n)" 写在代码中如下:
//对比标准字符串和原始字符串
#include <iostream>
using namespace std;
int main()
{
cout << "\"Hello, I'am limou3434.\"\n" << endl; //标准字符串
cout << R"(\"Hello, I'am limou3434.\"\n)" << endl; //原始字符串
return 0;
}标准字符需要使用 "" 包含起来,而原始字符串需要使用 R"()" 包含起来,C++ 还允许用户在 " 和 ( 或 ) 之间加入任意字符(除去空格、左右括号、斜杠、控制字符),用户只需要保证是以对称形式出现来包含字符串即可。
这么做的理由是防止原始字符串中出现 ") 导致结尾结束标志识别错误。
//使用自定义结束标志
#include <iostream>
using namespace std;
int main()
{
cout << R"**(\"Hello, I')" limou3434.\"\n)**" << endl; //原始字符串
return 0;
}11.C++语言调用C语言的细节
C++和C是有交叉调用的可能的,比如:“C++调用C的库,C调用C++的库”。这是可以实现的,但是会麻烦一些,需要做一些处理。12.1.C++语言调用C库
在 C++ 中调用C库有几种常见的方法,下面我介绍两种常用的方式:静态链接和动态链接。
### 11.1.1.静态链接
静态链接是将 C 库的代码编译为静态库(例如:“.lib”或“.a”文件),然后将其与你的C++代码一起编译为可执行文件。这种方式将库的代码嵌入到最终的可执行文件中。
要使用静态链接方式,你需要完成以下步骤:
1. 将C库的头文件复制到你的 C++ 项目中。
2. 在编译C++代码时,添加C库的源文件或静态库文件到编译选项中。
3. 在C++代码中包含C库的头文件,并调用库函数。
#include <iostream> extern "C" { #include "clibrary.h" } int main() { std::cout << "Calling C library function from C++" << std::endl; c_library_function(); return 0; }
在上述示例中,我们假设“clibrary.h”是C库的头文件,其中声明了“c_library_function”函数。在C++代码中,我们使用“extern "C"”来告诉编译器使用C的调用约定。
可能有人会问,不是可以使用cstdio等C++兼容Cde头文件么?这两种做法有什么区别么?有,至少在符号表上,c的函数是不允许重载的,而且有的时候需要调用现有的C接口。
11.1.2.动态链接
动态链接是将C库编译为共享库(例如:“.dll”或“.so”文件),在运行时动态加载和链接库。这种方式使得你可以在不重新编译代码的情况下更新或切换库。
要使用动态链接方式,你需要完成以下步骤:
1. 将C库的头文件复制到你的C++项目中。
2. 在编译C++代码时,添加对应的库文件的链接选项。
3. 在 C++ 代码中包含 C 库的头文件,并调用库函数。
#include <iostream> extern "C" { #include "clibrary.h" } int main() { std::cout << "Calling C library function from C++" << std::endl; c_library_function(); return 0; }
在上述示例中,我们假设“clibrary.h”是C库的头文件,其中声明了“c_library_function”函数。在C++代码中,我们使用“extern "C"”来告诉编译器使用C的调用约定。
当你构建和运行程序时,确保设置正确的库路径和链接选项,以便正确地访问和调用C库的函数。
* 先写处C语言的源文件(函数定义)和头文件(函数声明)
* 如果是VS2022中,把“配置属性->常规->配置类型”的“应用程序(.exe)”改成“静态库(.lib)”
* 运行C语言代码以生成静态库(或则动态库,这里演示静态库),生成的静态库就是以“.lib”为结尾的文件(实际上正常的程序在“链接”这个过程也会链接一些静态库和动态库,只不过这一次我们手动增加了一些)
* 生成静态库文件成功后,在另外一个C++代码中执行下面步骤,辅助链接
* 在“链接器->常规->附加库目录”引入包含静态库的目录
* 在“链接器->输入->附加依赖项->编辑”处引入静态库文件的名字
* 然后使用:extern "C"(而这一步是因为函数在生成符号表的时候,C语言和C++语言的符号表不一样)
* 然后生成就行
extern "C" { //引入C语言的头文件(函数定义) }12.2.C语言调用C++库
* 首先写好C++库,然后通过一样的方式得到静态库
* 然后写一个C代码,引入C++的头文件(函数声明)
* 设置好辅助链接的部分
* 在“链接器->常规->附加库目录”引入包含静态库的目录
* 在“链接器->输入->附加依赖项->编辑”处引入静态库文件的名字
* 然后也会出现一个问题:符号表的函数符号修饰不一样,这个时候就只能改C++语言,而不是C语言
* 因此在C++代码中,将函数定义的部分用extern括起来
* 但是在C代码中,由于在包含的时候包含了这个“extern "C"”语句,因此还需要特殊解决一下
* 方法一:利用C++有宏“__cplusplus”
* 方法二:略
## 11.3.“extern "C"”的含义
有的时候C++工程可能需要将某些函数按照C的风格来编译,在函数前加上“extern "C"”,其意思就是:“告知C++编译器,该函数按照C语言规则来编译,要用C的规则去链接查找函数。”之所以这样做的原因是C编写的代码没办法生成C++的符号表,但是C++是能够识别C的符号表的。