C++是在C的基础之上,容纳进去了面向对象编程思想,并增加了许多有用的库,以及编程范式等
文章目录
- 一、命名空间
- 二、输入输出
- 三、缺省参数
- 四、函数重载
- 五、引用
- 1. 引用的用法
- 2. 常引用
- 3. 引用的使用场景
- 4. 引用的底层实现
- 六、内联函数
- 七、auto 关键字(C++11)
- 八、范围 for -- 语法糖(C++11)
- 九、nullptr(C++11)
C++ 为了解决 C语言中存在的一些不好的地方,从而增加了一些语法
一、命名空间
在项目组中,进行代码合并时,可能会遇到自己和其他人写的代码,定义了相同的名字,发生命名冲突,在C语言中,只能在产生错误后,修改名字,没有很好的预防手段
自己定义的变量名,函数名,类型名等,都可能会和别人定义的名字冲突,或者和库的名字冲突,因为此时相同的名字处在同一个域(全局域)
而在不同的域定义了相同的名字,并不会产生命名冲突,使用时是局部优先
C++ 中增加了另一种新的域,叫做命名空间,用 namespace 关键字定义
命名空间定义:
namespace 空间名
{
//类型,变量,函数等
} //花括号后没有分号
将处在全局域中的变量,函数,类型等,封装在命名空间域中,便可以防止命名冲突
在命名空间域中也包含全局域和局部域,因此不会影响变量的生命周期
在命名空间域中,内部对内部的访问,也是遵守先声明后使用,优先使用局部,然后使用命名空间域中的全局,最后使用命名空间域外的全局
命名空间域会影响编译时期的查找规则,在查找一个变量、函数、类型是否定义时,默认是在局部找(代码块内),局部不存在便会去全局找,全局找不到便会报错,不会直接去已经定义的命名空间中找
外部想要访问命名空间内部的内容时:有三种方法
- 指定命名空间访问,使用域作用限定符 :: (两个冒号)
- 展开命名空间中的所有内容,using namespace 命名空间名
- 展开命名空间中的常用内容,using 命名空间名 :: 内容
C++ 中,将标准库函数的代码放在 iostream 头文件中,并用 std 命名空间封装,防止用户和库中的命名冲突,这里以 cout 为例
//指定命名空间访问
//:: 域作用限定符,左边为空,代表在全局域中查找
#include <iostream>
int main()
{
std::cout << "hello world";
return 0;
}
//展开命名空间中的所有内容
#include <iostream>
using namespace std;
int main()
{
cout << "hello world";
return 0;
}
//展开命名空间中的常用内容
#include <iostream>
using std::cout;
int main()
{
cout << "hello world";
return 0;
}
平时练习时可以使用全部展开,写项目时建议展开常用的和指定访问
还需要注意几点
- 命名空间的名字相同时,会合并为一个命名空间,但是名字相同的命名空间中,如果存在相同的定义会报错
- 命名空间可以定义在命名空间中,即命名空间可以嵌套定义
二、输入输出
C++ 中标准库提供的 cin 可以用于输入,cout 可以用于输出,cin 和 cout 自动识别类型,不用指定输入输出格式,cout 中 endl 表示换行
#include <iostream>
//为了可以找到封装在 std 中的 cin 和 cout
using namespace std;
int main()
{
char c;
int i;
double d;
//自动识别类型,cin >> 可以简单理解为数据从控制台流向变量
cin >> c >> i >> d;
//自动识别类型,输出数据后换行,cout << 可以简单理解为数据从变量流向控制台
cout << c << endl;
cout << i << endl;
cout << d << endl;
cout << "hello C++" << endl;
return 0;
}
三、缺省参数
缺省参数是声明或定义函数时为函数的参数指定一个缺省值,在调用该函数时,如果省略指定实参则采用该形参的缺省值,否则使用指定的实参。
声明或定义函数时只能从右往左连续的为函数的参数指定缺省值,调用函数传参时,也只能从右往左连续省略实参
#include <iostream>
using namespace std;
//只能从右往左连续的为函数的参数指定缺省值
//void f1(int a, int b = 3, int c) -- 错误写法
void f1(int a, int b = 3, int c = 10)
{
cout << a << " ";
cout << b << " ";
cout << c << endl;
}
int main()
{
//只能从右往左连续省略实参
//f1(1, , 3); -- 错误写法
f1(1);
f1(1, 2);
f1(1, 2, 3);
return 0;
}
注意:
- 有缺省参数时,声明和定义不能同时给缺省参数,推荐在声明给,如果出现声明和定义分离(多文件时),只能在声明给,定义时不给
- 缺省值只能是常量或全局变量(一般不使用全局变量)
//报错:非法将局部变量作为缺省参数
void f1(int a, int b = a)
{
cout << a << endl;
}
四、函数重载
C++ 允许 在同一作用域中 声明几个功能类似的 同名函数,但是需要满足这些同名函数的形参列表(参数个数 或 参数类型 或 参数类型顺序)不同,常用来处理实现功能类似,但参数的数据类型不同的问题
#include <iostream>
using namespace std;
//参数个数不同
void f1()
{
cout << "f1()" << endl;
}
void f1(int a)
{
cout << "f1(int a)" << endl;
}
//参数类型不同
int Add(int A, int B)
{
return A + B;
}
double Add(double A, double B)
{
return A + B;
}
//类型顺序不同
void f2(int i, char c)
{
cout << "f2(int i, char c)" << endl;
}
void f2(char c, int i)
{
cout << "f2(char c, int i)" << endl;
}
int main()
{
//参数个数不同
f1();
f1(1);
//参数类型不同
cout << Add(1, 2) << endl;
cout << Add(1.1, 2.2) << endl;
//参数顺序不同
f2(1, 'a');
f2('a', 1);
return 0;
}
未重载的函数,调用函数传参类型不同时,会隐式转换
重载的函数,调用函数时,实参的参数列表 和 所有重载的函数的参数列表 都不匹配时,不会隐式转换,编译器无法分辨调用哪个函数,便会报错
C++ 函数重载的原理 – 对函数名修饰
以 Linux 下的 gcc/g++函数名修饰规则为例
用 gcc 编译 .c 文件后,用 objdump -S 可执行程序 指令,查看函数名修饰规则
用 g++ 编译 .cpp 文件后,用 objdump -S 可执行程序 指令,查看函数名修饰规则
在编译时期对函数名进行修饰之后,便可以根据调用函数时给的参数类型,确定函数调用的是哪个重载函数,因此重载函数只会影响编译速度
函数调用时,返回值可以选择性的使用,因此只有返回值不同的同名函数,调用时无法区分,便不能构成重载
五、引用
1. 引用的用法
//定义引用
//类型& 引用名 = 引用实体;
int a = 10;
int& ra = a;
在使用上:引用不是定义新变量,而是给指定的变量取别名,和该变量共用同一块空间
#include <iostream>
using namespace std;
int main()
{
//定义变量 i
int i = 0;
//定义新变量 j, i 和 j 占用不同的空间
int j = i;
//给变量 i 取别名, i 和 k 占用相同的空间
int& k = i;
//i 和 j 的地址不同
//i 和 k 的地址相同
cout << &i << " ";
cout << &j << " ";
cout << &k << endl;
++i;
++j;
++k;
//结果为 2 1 2
cout << i << " ";
cout << j << " ";
cout << k << endl;
return 0;
}
注意:
- 引用是变量的别名
- 引用在定义时必须初始化
- 变量可以有多个引用,可以给引用初始化为变量的别名(给别名取别名,都是同一个变量)
- 引用指定实体后,不能在引用其他实体
- 没有 NULL 引用
- 在 sizeof 中,引用结果为引用类型大小
- 没有多级引用,引用指针,引用数组,可以按以下内容理解
引用不需要开辟新空间,没有引用的引用
引用不需要开辟新空间,没有引用的指针
引用不需要开辟新空间,不能作为数组元素(数组元素有空间),即不存在引用数组
2. 常引用
C++ 中,指针和引用在使用时,权限只能保持或缩小,但是 权限不能放大
先以指针为例:
#include <iostream>
using namespace std;
int main()
{
//指针权限放大 - error
//i 是常变量,不可以被修改
//int* 可以通过解引用修改变量的值
//不可以修改 --> 可以修改 权限放大(不允许)
const int i = 0;
int* pi = &i; //报错,const int* 无法转换为 int*
//指针权限保持
//i 是常变量,不可以被修改
//const 修饰 *pi,因此 pi 不能通过解引用修改变量 i 的值
//不可以修改 --> 不可以修改 权限保持
const int i = 0;
const int* pi = &i;
//指针权限缩小
//i 是变量,可以被修改
//const 修饰 *pi,因此 pi 不能通过解引用修改变量 i 的值
//可以修改 --> 不可以修改 权限缩小
int i = 0;
const int* pi = &i;
return 0;
}
引用也是如此:
#include <iostream>
using namespace std;
int main()
{
//引用权限放大 - error
const int i = 0;
int& ri = i; //报错,const int 无法转换为 int&
int& r = 10; //报错
//引用权限保持
const int i = 0;
const int& ri = i;
const int& r = 10;
//引用权限缩小
int i = 0;
const int& ri = i;
return 0;
}
3. 引用的使用场景
(1) 输出型参数:
#include <iostream>
using namespace std;
//引用做参数,可以直接修改实参的值
void Swap(int& r1, int& r2)
{
int tmp = r1;
r1 = r2;
r2 = tmp;
}
int main()
{
int a = 10, b = 20;
cout << a << " " << b << endl;
//直接传变量 a 和 b;
Swap(a, b);
cout << a << " " << b << endl;
return 0;
}
输出:
10 20
20 10
(2) 返回值引用:
在函数栈帧的知识中,函数的返回值是通过临时变量返回的(为什么不能直接返回?因为函数调用结束,回到调用函数处时,被调用函数的栈帧已经还给操作系统了),如果返回值较小,则使用寄存器,如果返回值较大,则在调用函数的栈帧中开辟
下述代码中,变量 n 在静态区,当函数调用结束,回到调用函数的栈帧中时,变量 n 不会被销毁,但是编译器还是会通过临时变量的方式带回返回值
int Count()
{
static int n = 0;
n++;
// ...
return n;
}
int main()
{
int ret = Count();
return 0;
}
此时程序员可以自行调整代码,将返回值设置为引用,便不会使用临时变量,而是使用原空间的别名
int& Count()
{
static int n = 0;
n++;
// ...
return n;
}
int main()
{
int ret = Count();
return 0;
}
注意:不要返回当前函数栈帧中的变量的引用
#include <iostream>
using namespace std;
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
//ret 的结果是随机值
cout << "Add(1, 2) is :" << ret << endl;
return 0;
}
什么情况下可以返回引用:调用函数结束,回到调用函数处时,变量不会销毁的,都可以做为引用返回
如:在堆上开辟的空间,静态区,参数中的引用等
引用做返回值时,’ 可以对函数的返回值进行修改 ',在之后学习的运算符重载中作用很大
传引用明显的比传值效率高
参数传值时,形参是实参的临时拷贝,返回值传值时,使用的是临时变量
参数传引用时,形参是实参的别名,返回值传引用时,使用的是变量的别名
4. 引用的底层实现
实现上:引用是由指针实现的,也是需要开辟空间的,虽然实现上是这样,但是为了便于使用引用,还是以使用上的概念为主
int main()
{
int a = 0;
//实现上,将 a 的地址赋给指针 ra
int& ra = a;
//实现上,编译器默认将 ra 解释为(*ra)
//因此,&ra 解释为 &(*ra) == &a 所以 &a 和 &ra 的地址一样
ra = 10;
int* pa = &a;
*pa = 10;
return 0;
}
//启动调式,右键进入反汇编
int a = 0;
00007FF71DA317AD mov dword ptr [a],0
//引用和指针的指令,在汇编层面一模一样
int& ra = a;
00007FF71DA317B4 lea rax,[a]
00007FF71DA317B8 mov qword ptr [ra],rax
ra = 10;
00007FF71DA317BC mov rax,qword ptr [ra]
00007FF71DA317C0 mov dword ptr [rax],0Ah
int* pa = &a;
00007FF71DA317C6 lea rax,[a]
00007FF71DA317CA mov qword ptr [pa],rax
*pa = 10;
00007FF71DA317CE mov rax,qword ptr [pa]
00007FF71DA317D2 mov dword ptr [rax],0Ah
引用类型必须和引用实体是同种类型的,不同类型无法直接赋值,但可以通过以下两种方式
在 C/C++ 中,显示/隐式类型转换是通过临时变量来完成的,临时变量具有常属性(不可以被修改)
#include <iostream>
using namespace std;
int main()
{
int a = 2;
double& ra1 = (double&)a;
const double& ra2 = a;
//输出 2 -9.25596e+61 2
cout << a << " " << ra1 << " " << ra2 << endl;
return 0;
}
//启动调式,右键进入反汇编
int a = 2;
00007FF7780D23CD mov dword ptr [a],2
//mov 将 2 放到 a 中
double& ra1 = (double&)a;
00007FF7780D23D4 lea rax,[a]
00007FF7780D23D8 mov qword ptr [ra1],rax
//lea 取出 a 的地址放到 rax
//mov 将 rax 中的内容放到指针 ra1 中
const double& ra2 = a;
00007FF7780D23DC cvtsi2sd xmm0,dword ptr [a]
00007FF7780D23E1 movsd mmword ptr [rbp+68h],xmm0
00007FF7780D23E6 lea rax,[rbp+68h]
00007FF7780D23EA mov qword ptr [ra2],rax
//cvtsi2sd 取出 a 中低 64 位,将其转换为浮点数,放到 xmm0
//movsd 将 xmm0 放到临时变量 rdb+68h(h 表示十六进制) 空间中
//lea 取出 rdb+68h 空间的地址放到 rax
//mov 将 rax 中的内容放到指针 ra2 中
引用 ra1 是 a 的别名,因此 ra1 会以浮点数的方式看待 a 中的值(这里可以修改 ra1,存在问题,待以后解决)
引用 ra2 是赋值时隐式转换过程中 临时变量的别名,因此 ra2 是以浮点数的方式看待浮点类型的临时变量的值
引用比指针使用起来更安全
六、内联函数
在 C语言中,宏的缺点:1. 不能调试 2. 没有类型检查 3. 有些场景下非常复杂,容易出错,宏的优点:1. 速度快(不用创建函数栈帧) 2. 类型不固定
在 C++ 中,为了弥补宏的缺点,推荐 用 const 和 enum 代替 宏常量,用 内联函数 代替 宏函数
用 inline 修饰的函数称作内联函数(可以调试,有类型检查,书写和函数一样容易记忆),编译时 C++ 编译器会 在调用内联函数的地方展开(速度快),
为了可以在 debug 模式观察到内联函数的展开,需要对编译器进行设置(因为 debug 模式下,编译器默认不会对代码进行优化)
在 vs 2022 中
右击项目 -> 属性 -> C/C++ 下的常规 -> 调式信息格式 改为 程序数据库 (/Zi)
右击项目 -> 属性 -> C/C++ 下的优化 -> 内联函数扩展 改为 只适用于 __inline (/Ob1)
#include <iostream>
using namespace std;
//内联函数
inline void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 10, b = 20;
Swap(a, b);
cout << a << " " << endl;
return 0;
}
//启动调式,右键进入反汇编
//汇编指令中,函数调用时需要通过 call 指令,跳转到函数地址
//Swap 调用时的汇编指令中没有 call,也就说明内联函数展开了
Swap(a, b);
00007FF64AB41552 mov eax,dword ptr [a]
00007FF64AB41556 mov dword ptr [rsp+60h],eax
00007FF64AB4155A mov eax,dword ptr [b]
00007FF64AB4155E mov dword ptr [a],eax
00007FF64AB41562 mov eax,dword ptr [rsp+60h]
00007FF64AB41566 mov dword ptr [b],eax
内联函数特性:
- 函数用 inline 修饰,对编译器而言只是一个建议,在编译阶段是否会展开,取决于编译器,不加 inline 修饰的函数不会展开
建议 将函数规模小,不是递归并且频繁调用的函数 用 inline 修饰,否则编译器会忽略 inline 特性 - 内联函数是一种以空间换时间的方法
优点:少了函数调用开销,提升效
缺点:可执行程序变大 - 内联函数如果声明和定义分离,会导致链接错误
编译时会将 F.h 中的声明拷贝到 test.cpp 中,此时 test.cpp 中只有内联函数 f 的声明,没有内联函数 f 的定义,于是便无法将 f 函数展开(.cpp 文件是单独编译的),并且由于内联函数会展开,所以内联函数 f 并不会在 test.cpp 的符号表中产生,也就导致在链接时合并符号表后无法找到函数 f
书写内联函数时,在 .h 文件中 定义
七、auto 关键字(C++11)
在早期的 C/C++ 中 auto 用来表示变量具有自动存储器的局部变量(但是一直没有人使用 auto)
到了 C++11,标准委员会赋予了 auto 全新的含义,auto 不再是一个存储类型指示符,而是一个新的类型指示符(为了避免与 C++98 中的 auto 发生混淆,C++11 只保留了 auto 作为类型指示符的用法)
auto 声明的变量,由编译器在编译时期 根据初始化的值的类型 自动推到变量的类型
#include <iostream>
using namespace std;
int main()
{
//auto 声明的变量必须初识化
//auto a; --error
//auto 自动推导变量类型
auto c = 'a';
auto i = 10;
auto d = 3.14;
//auto 声明指针时 auto 和 auto* 没有区别,但 auto* 一定要初识化为地址
auto p1 = &i;
auto* p2 = &i;
//引用类型必须用 auto&
auto& r = i;
//typeid 是操作符,typeid(变量).name() 获取变量的类型
//输出:char int double int * __ptr64 int * __ptr64 int
cout << typeid(c).name() << " ";
cout << typeid(i).name() << " ";
cout << typeid(d).name() << " ";
cout << typeid(p1).name() << " ";
cout << typeid(p2).name() << " ";
cout << typeid(r).name() << endl;
return 0;
}
auto 不是一种 “类型” 的声明,而是一种类型声明时的 “占位符”,编译器在编译期会将 auto 替换为变量实际的类型,所以 auto 声明多个变量时,编译器只会对第一个变量的类型进行推导,然后将 auto 替换为推导的类型,因此每个变量的类型应该相同(不相同会编译错误)
auto 不能作为函数的参数,因为编译时无法知道实参的类型
auto 不能用来声明数组
在以后的学习中,某些类型较长,容易写错,此时便可以使用 auto 关键字自动推导,这种情况下也可以使用 typedef 来解决,不过 typedef 在某些情况存在一些问题
typedef char* pstr;
int main()
{
//编译报错,const 修饰 p1,需要指定初始化
//const pstr p1;
//const 修饰 p2,编译通过
const pstr* p2;
return 0;
}
八、范围 for – 语法糖(C++11)
C++11 中引入了基于范围的 for 循环
范围 for 循环后的括号由冒号 : 分为两部分
第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
#include <iostream>
using namespace std;
int main()
{
int array[] = { 1, 2, 3, 4, 5, 6 };
//自动将数组 array 中的元素依次赋值给 i,自动判断结束
//i 是循环变量,auto 是循环变量的类型
for (auto i : array)
{
cout << i << " ";
}
cout << endl;
return 0;
}
注意:和普通循环一样,可以使用 continue 和 break
九、nullptr(C++11)
在定义指针变量时,如果不知道指针的指向,通常会将指针变量指向 NULL
在 C++98 中,NULL实际是一个宏,在传统的 C 头文件 stddef.h 中
//在 C++ 中,NULL 被定义成 0
//在 C 语言中,NULL 被定义成 ((void *)0)
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
由于在 C++98 中 NULL 被定义为了 0,在使用 NULL 时,难免会遇到一些麻烦
#include <iostream>
#include <stddef.h>
using namespace std;
void f(int a)
{
cout << "f(int)" << endl;
}
void f(int* p)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(NULL);
return 0;
}
输出:
f(int)
f(int)
程序本意是想通过 f(NULL) 调用指针版本的 f(int*) 函数,但是由于 NULL 被定义成 0,因此与程序的初衷相悖
C++11 中,引入了新的关键字 nullptr,表示指针空值((void*)0),sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同