【C++】1-入门
1. 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 | thro |
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. 命名空间
域
(一般情况)域限定了域中的变量和函数的生命周期和作用范围。
C语言中,我们知道,同一个域只能定义一个同名变量,不同域可以定义同名变量。
看下面这段代码:
#include <stdio.h> int a = 10; // int a = 20; 同一个域中,a变量重定义,报错 int main() { int a = 20;// 不同域中,可以再次定义a变量 printf("%d\n", a);//打印20 -- 局部优先原则 (现在局部域找,局部找不到就到全局域找) return 0; }
如果我们要输出的是全局域中的a,而不是main函数局部域中的a呢?
// g++环境下执行 #include <stdio.h> int a = 10; int main() { int a = 20; printf("%d\n", ::a);//打印10 -- 限定域是全局域 return 0; }
::
是域作用访问限定符,可以指定某个作用域中查找变量或函数。
::
左边是(限定)域,右边是查找目标。
- 左边是空白(即不指定限定域):默认是全局域中查找
- 左边是命名空间域名:是只在该命名空间域中查找
注意:
::
域作用限定符支持C++,但不支持C。
C++中可能会出现用户自己定义的变量名称和C/C++标准库中提供的函数名相同等的命名冲突问题,命名空间就可以很好地解决这个问题。
报错:编译器不能确定要打印的rand是全局变量rand,还是stdlib库中的一个函数名称。
2.1 命名空间定义
#include <stdio.h>
#include <stdlib.h>
//int rand = 10;
namespace t1 // 命名空间
{
int rand = 10;
}
int main()
{
printf("%d\n", rand);// stdlib中的函数地址
printf("%d\n", ::rand);// stdlib中的函数地址
printf("%d\n", t1::rand);// 10
return 0;
}
上述代码中:
-
命名空间t1中的rand依然是全局变量,只是对其增加了一个访问限制,相当于给命名空间中的内容加了一座隔离墙,影响编译查找规则。(只有定义在函数里面的才是局部变量,原因参考函数栈帧)
-
第二个printf为什么也打印的是函数地址?
编译器查找规则:默认先从局部域找,找不到就去全局域找,(编译时,头文件stdlib.h在预处理阶段就展开)。
- 若没有指定要查找的命名空间域,全局域中查找时是不会到命名空间域查找的。进不去墙内。
- 若指定了要查找的命名空间域,便直接到该命名空间域中查找。相当于直接到墙内查找。
命名空间只是对其内的变量或函数增加了访问限制,但它们的生命周期和作用域依然不变。
2.2 命名空间使用
-
默认
#include <iostream> int main() { std::cout << "hello world" << std::endl; // hello world return 0; }
-
命名空间全部展开
#include <iostream> using namespace std;// 将命名空间std展开 int main() { cout << "hello world" << endl;// hello world return 0; }
优点:可以直接使用被展开的命名空间中的内容。
缺点:不安全。例如:命名冲突。
-
命名空间部分展开
#include <iostream> using std::cout;// 只展开cout int main() { cout << "hello world" << std::endl;// hello world return 0; }
std:C++官方库内容定义的命名空间
3. 输入/输出
输入:cin
#include <iostream>
using namespace std;
int main()
{
int a;
char b;
cin >> a >> b;
cout << a << ' ' << b;
return 0;
}
输出:cout
#include <iostream>
using namespace std;
int main()
{
int a = 1;
float b = 1.0f;
double c = 10.1;
char d = 'A';
char str[] = "hello world";
cout << a << endl;
cout << b << ' ' << c << endl;
cout << d << '\n';
cout << str << endl;
return 0;
}
cin
– console in:控制台输入
cout
– console out:控制台输出
endl
– end line:相当于换行
>>
:流提取运算符
<<
:流插入运算符
优点:自动识别类型。(本质是函数重载)
4. 缺省参数
顾名思义,某些函数可以不传参。
#include <iostream>
using namespace std;
void func(int a = 10)// 参数a有一个缺省值10,或者默认值
{
cout << a << endl;
}
int main()
{
func();// 打印10
func(100);// 打印100
return 0;
}
对于有缺省值的参数,若不传参,该参数的值就默认是缺省值;若传参,该参数的值就是传的实参的值。
4.1 全缺省
函数的所有参数都给缺省值。
#include <iostream>
using namespace std;
void func(int a = 10, int b = 20, int c = 30)
{
cout << a << ' ' << b << ' ' << c << endl;
}
int main()
{
func();// 打印10 20 30
func(100);// 打印100 20 30
func(100, 200);// 打印100 200 30
return 0;
}
对于全缺省,若传实参,是从左往右连续传参的!
4.2 半缺省/部分缺省
只有函数的部分参数给缺省值。
#include <iostream>
using namespace std;
// 错误写法 -- 从左往右连续缺省
//void func(int a = 10, int b = 20, int c)
//{
// cout << a << ' ' << b << ' ' << c << endl;
//}
// 正确写法 -- 从右往左连续缺省
void func(int a, int b = 20, int c = 30)
{
cout << a << ' ' << b << ' ' << c << endl;
}
int main()
{
func();// 报错,半缺省必须传参数
func(100);// 打印100 20 30
func(100, 200);// 打印100 200 30
//func(100, , 300);// 错误写法
return 0;
}
对于半缺省,只能从右往左连续缺省!传参时,同全缺省,也只能从左向右连续传参!
注意:以上的传参顺序仅仅指的是传实参的顺序。并不研究实参传给形参的顺序,这是由编译器实现的。
**缺省参数不能在声明和定义中同时出现!!!**只能在声明中出现,而定义中的参数不缺省。
(以防声明和定义的缺省参数值不同)
5. 函数重载
5.1 函数重载的概念及用法
参数的个数、类型、顺序不同的同名函数可以同时存在。即对于同一个名字的函数,可以实现多种功能。
例:
#include <iostream>
using namespace std;
int Add(int x, int y)
{
cout << "Add(int, int)" << endl;
return x + y;
}
// 参数类型不同
double Add(double x, double y)
{
cout << "Add(double, double)" << endl;
return x + y;
}
// 参数个数不同
int Add(int x, int y, int z)
{
cout << "Add(int, int, int)" << endl;
return x + y + z;
}
// 参数顺序不同
double Add(int x, double y)
{
cout << "Add(int, double)" << endl;
return x + y;
}
double Add(double x, int y)
{
cout << "Add(double, int)" << endl;
return x + y;
}
int main()
{
cout << Add(10, 20) << endl;// 30
cout << Add(1.1, 2.2) << endl;// 3.3
cout << Add(10, 20, 30) << endl;// 60
cout << Add(1, 1.1) << endl;// 2.1
cout << Add(2.2, 1) << endl;// 3.2
return 0;
}
// 输出
Add(int, int)
30
Add(double, double)
3.3
Add(int, int, int)
60
Add(int, double)
2.1
Add(double, int)
3.2
注意:
#include <iostream>
using namespace std;
void func()
{
cout << "func()" << endl;
}
void func(int a = 1, int b = 2)
{
cout << "func(int, int)" << endl;
}
int main()
{
func(1);// 正常执行
func(1, 2);// 正常执行
func();// 运行错误
return 0;
}
对于无参函数的重载(如上)易出现“对重载函数的调用不明确”的运行错误。(二义性)
5.2 函数重载的原理认识
C语言不支持函数重载,但C++支持。那么C++是如何支持函数重载的呢?(即C++是如何实现对同名函数的多种不同且精准的调用方式?)
函数调用本质是call被调用函数的地址来调用该函数。
函数名和函数地址是存在于符号表中的,且函数名经特定的修饰规则修饰:
// C语言符号表中的函数名就是开发者写的函数名
// C++符号表中中存放的函数名是经过修饰的函数名
// C++符号表中函数名和函数地址的存放
void f(int a = 1)
{
cout << "f(int)" << endl;
}
void f(int a = 1, double b = 2.0)
{
cout << "f(int, double)" << endl;
}
void f(double a = 1.1, int b = 2)
{
cout << "f(double, int)" << endl;
}
// 符号表中函数名和地址的存放(例如下)
// 函数名:函数地址
// _z1fi:0x00ff11
// _z1fid:0x00ff33
// _z1fdb:0x00ff55
// (Linux下g++)函数名修饰规则:
// 1:表示函数名长度
// f:表示函数名称
// i:表示有一个参数,该参数是int类型
// id:表示有两个参数,第一个参数是int类型,第二个参数是double类型
// di:表示有两个参数,第一个参数是double类型,第二个参数是int类型
已经知道同名函数参数不同构成函数重载是因为函数名被特定修饰规则修饰进而能被编译器区分,那么,理论上返回值类型也可被添加到函数名修饰规则中,为什么返回值类型不同不能构成函数重载呢?
int func(int a = 1)
{
cout << "int func(int)" << endl;
}
void func(int a = 1)
{
cout << "void func(int)" << endl;
}
// 对返回值类型也进行名称修饰
// _z4funcii:int int
// _z4funcvi:void int
// 单看修饰后的名称,没有问题
int main()
{
// 报错
func(10);
func(10);
//调用时,不指定返回值类型,不能区分调用的是哪个函数 -- 调用时的二义性
return 0;
}
6. 引用
6.1 引用的概念
逻辑上通俗的说,引用相当于对被引用对象起别名。编译器不会对新变量另开空间,原变量和新变量共用同一块存储空间。
// 类型& 变量名或对象名 = 引用实体
int main()
{
int a = 0;
int& b = a;
int& c = b;
int& d;// 编译错误,引用必须被初始化
cout << a << " " << b << " " << c << endl;
a++;
cout << a << " " << b << " " << c << endl;
b++;
cout << a << " " << b << " " << c << endl;
c++;
cout << a << " " << b << " " << c << endl;
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
return 0;
}
// 输出
0 0 0
1 1 1
2 2 2
3 3 3
00D3F960
00D3F960
00D3F960
6.2 引用特点
- 引用在定义时必须被现有变量初始化
- 一个变量可以有多个引用
- 引用一旦引用了一个实体,则该引用无法再引用其他实体
int main()
{
int a = 10;
int b = 20;
int& ra = a;
int& rra = a;
int& rb;// 编译错误
cout << a << " " << b << endl;
ra = b;// 赋值操作,而非改变被引用实体
cout << a << " " << b << endl;
return 0;
}
// 输出
10 20
20 20
6.3 常引用
void Test1()
{
int a = 1; // 权限:可读可写
// 权限平移 -- int -> int&
int& ra = a;// 权限:可读可写
const int b = 1;// 权限:只读
// 权限放大 -- const int -> int&
int& rb = b;// 权限:可读可写 报错 -- 无法从“const int”转换为“int &”
int c = 2;// 权限:可读可写
// 权限缩小 -- int -> const int&
const int& rc = c;// 权限:只读
const int d = 4;// 权限:只读
// 权限平移
const int& rd = d;// 权限:只读
}
在指针和引用的赋值操作中,权限可以缩小,但不能放大!
同时,权限的放大和缩小也仅限于指针和引用的赋值中!
void Test2(int tmp)
{
int a = 0;
const int b = 1;
int c = 2;
// const int -> int
a = b;
// 按理说,权限放大是不允许的,但是,权限的放大和缩小仅限于指针和引用的赋值操作中
// 这里a = b仅是值的拷贝。
}
int main()
{
const int a = 10;
Test2(a);
return 0;
}
// 以上代码正常编译运行
引用实体为常量:
// 例1
void func(const int& a = 10)
{}
int main()
{
const int a = 1;
double d = 1.1;
int r = d;// 类型转换 传给r的实际上是由double d产生的int类型的临时变量
int& rd = d;// 同理,传的也是临时变量
// 但是,因为临时变量具有常性,所以本质是不能由const int -> int&,权限放大了
const int& rrd = d;// 这样就可以了
return 0;
}
从上述代码中可以得出结论:临时变量具有常性!
// 例2
// 这段代码不推荐!只是举例,仅此而已
int Count()
{
int n = 0;
n++;
/**/
return n;
}
int main()
{
int& ret = Count();// 报错 -- “初始化”: 无法从“int”转换为“int &”
// 实际原因是:Count返回的是临时变量,但临时变量具有常性,因此,接收返回值的变量也应具有常性
const int& _ret = Count();// 这样就可以了
return 0;
}
6.4 引用的应用
6.4.1 引用做参数
实际上,这里的参数指的是输出型参数。
输入型参数和输出型参数
int Add(int x, int y)
{
return x + y;
}
// x和y就是输入型参数
// x和y所对应的实参仅仅做参数传递给形参供函数使用,而函数对实参不影响
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
// x和y是输出型参数
// x和y所对应的实参不仅做参数传递给形参供函数使用,且函数对实参的值有影响
// 如本例,函数Swap会将x和y对应的实参的值对调
注意:
void func(int& a)
{
}
int main()
{
const int a = 1;
func(a);// 报错 -- 权限放大 无法将参数 1 从“const int”转换为“int &”
return 0;
}
参考常引用特点,建议引用做参数时,尽量使用常引用做参数!
6.4.2 引用做返回值
// 引用返回
int& Count()
{
static int n = 0;// n不存储在函数Count的栈帧里,而是存在于静态区中
n++;
/**/
return n;
}
// 返回的就是static int定义的变量n本身
// 也可理解为返回的也是一个临时变量,但该临时变量的类型是int&,所以该临时变量实际上是n的别名,不会再额外开辟存储空间。
// 传值返回
int Count()
{
// 若n非static修饰
int n = 0;// 存在于函数Count的栈帧里
n++;
/**/
return n;
}
int main()
{
int ret = Count();
return 0;
}
// 不会返回函数Count栈帧中定义的局部变量n本身
// 因为执行int ret = Count();语句时,函数Count的栈帧已经被销毁,其中的数据将不再受保护,可能会改变(被其他操作覆写,或被销毁变为默认的随机值),数据是不确定的、未定义的
// 所以,在Count的栈帧销毁前,编译器会生成一个临时变量(类型和函数返回值类型一致)例tmp来保存n的值
// 返回的实际上是临时变量tmp
// 关于临时变量,若n占用空间大小较小,则临时变量存储在寄存器中
// 若n占用空间较大,则临时变量存储在函数Count的上一层栈帧,即main函数的栈帧中
注意:
- 出了函数作用域,若返回变量不再存在,则不能使用引用返回
- 处理函数作用域,若返回变量依然存在,则可以使用引用返回
优点:
- 相比于传值返回,引用返回没有返回变量的临时拷贝,提高了代码效率。
- 可以修改返回值。
6.5 指针和引用的区别
对于指针:
- 创建一个指针变量指向一个实体,对该指针变量开辟存储空间。
对于引用:
- 语法上,引用相当于对引用实体取别名,不开辟存储空间
- 实际底层实现上,引用是用指针实现的,需要开辟空间
引用底层实现:
// 反汇编
int a = 10;
008E184F mov dword ptr [a],0Ah
int& ra = a;
008E1856 lea eax,[a]
008E1859 mov dword ptr [ra],eax
int b = 20;
008E185C mov dword ptr [b],14h
int* rb = &b;
008E1863 lea eax,[b]
008E1866 mov dword ptr [rb],eax
从上述代码可以看出:
- 指针和引用都是(以指针为例),取b的地址到寄存器eax中,然后将寄存器eax中存放的地址移动/赋值到指针变量rb中。引用也是如此。
- 即使看不懂汇编代码,通过比对,也可以看出指针和引用的汇编代码可以说是一样的。
- 综上,引用在底层是用指针实现的!也需要开辟存储空间。
7. auto关键字
auto是C++11新特性。
特点:自动推导变量类型
int main()
{
int a = 10;
// auto:根据a的类型自动推导ra的类型
auto ra = a;
auto pa = &a;
auto* pa1 = &a;// 指定变量类型为指针类型 -- 等价于上一行写法
auto& rra = a;// 指定引用
cout << typeid(ra).name() << endl;// int
cout << typeid(pa).name() << endl;// int*
cout << typeid(pa1).name() << endl;// int*
cout << typeid(rra).name() << endl;// int
// typeid(变量).name -- 可以获取变量类型的字符串
return 0;
}
同一行定义多个变量:
auto在同一行定义多个变量时,这些变量的类型必须一致,auto会自动推导第一个变量的类型,其余的变量类型以这个变量的类型为准。
int main()
{
int a = 10;
auto b = a, c = 1;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
auto d = a, e = 1.1; // 变量类型不一样,报错 -- 在声明符列表中,“auto”必须始终推导为同一类型
return 0;
}
8. 范围for遍历
语法:
for(auto [agentName] : [arrayName])
{}
特点:
- 依次取数组arrayName中的数据赋值给agentName
- 自动判断结束
- 自动迭代
例:
// 读取
int main()
{
int array[] = { 1, 2,3,4,5,6,7,8,9,10 };
// 寻常for遍历
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
{
cout << array[i] << " ";
}
cout << endl;
// 范围for遍历
// for(int e : array) -- 这样也是可以的
for (auto e : array)
{
cout << e << " ";
}
cout << endl;
return 0;
}
// 输出
1 2 3 4 5 6 7 8 9 10
1 2 3 4 5 6 7 8 9 10
上面讲的都是基于auto的范围for读取,那么,如果要写入呢?
int main()
{
int arr[] = { 1, 2, 3, 4, 5 };
for (auto e : arr)
{
e *= 2;// 修改
}
for (auto e : arr)
{
cout << e << " ";
}
cout << endl;
return 0;
}
// 输出
1 2 3 4 5
通过输出结果可以知道,对于数组arr的修改并没有生效,这是为什么呢?
– 前面已经提到过,基于auto的范围for遍历,只是依次取出arr中的元素数据赋值给e,而我们修改的也只是e而已,对arr中的数据是没有影响的。
那么,如何在范围for中成功写入数据呢?
实际也很简单,只要做到对e的修改也会影响到arr中数据就可以了。
前面已经讲过引用,知道了引用的特性,那么就可以通过引用实现范围for中的写入了。
int main()
{
int arr[] = { 1, 2, 3, 4, 5 };
for (auto& e : arr)
{
e *= 2;// 修改
}
for (auto e : arr)
{
cout << e << " ";
}
cout << endl;
return 0;
}
// 输出:
2 4 6 8 10
范围for使用条件:
-
必须for循环的范围
对于数组,范围就是首元素到尾元素
// 以下代码不能使用范围for // 因为数组做参数传的是指针,而非数组 void Test(int arr[]) { for (auto e : arr) { cout << e << endl; } }
9. 内联函数
9.1 概念
被inline修饰的函数称作内联函数。
编译时,C++编译器会在调用内联函数的位置将内联函数展开,因此没有函数建立栈帧的开销,提高了程序员的运行效率。
不使用内联:
使用内联:
可以通过汇编代码看出,内联函数是没有函数调用的,在编译期间编译器会用函数体替换函数的
调用。
查看内联是否展开:
- 在release模式下,查看编译器生成的汇编代码中是否存在call Add
- 在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不
会对代码进行优化,下面给出vs2019的设置方法)
宏也可以不展开,那么为什么不用宏,反而创造出内联呢?
宏缺点:
- 不能调试
- 没有类型的安全检查
- 容易写错
内联就完美解决了宏的缺点。
9.2 使用事项
-
哪些函数适合设置为内联函数
并不是所有的函数都适合设置为内联函数。比如,递归函数或其他非常长的函数。
建议:频繁被调用的规模小的函数(10行以内)可以设置为内联函数。
为什么规模大的函数不展开?-- 会引起代码膨胀
举例说明代码膨胀:
一个函数func中有30行代码,调用该函数10000次,单对调用函数而言:
若不展开func,编译后,总共有func本身的代码30行 + 调用指令10000行 = 10030行;
若展开func,编译后,总共有func本身的代码30行 * 调用次数 = 300000行(不展开中调用的指令被替换为func本身)
补充:编译后的指令影响的只是生成的二进制可执行程序的大小。
-
内联函数是否展开实际取决于编译器
inline只是相当于一个建议,该建议是否采纳执行取决于编译器。
(内联说明只是向编译器发送一个请求,编译器可以选择忽略这个请求)
-
inline不建议声明和定义分离,否则会导致链接错误。(inline修饰的函数的函数地址不进符号表)
-
inline是一种以空间换时间的做法(这里的空间指的是编译生成的可执行程序的空间大小)
优点:提高程序运行效率
缺点:可能使生成的目标可执行程序文件所占用存储空间变大
10. 指针空值nullptr
C语言中NULL可以代表指针空值,但在C++中不可以。
// C语言
void func1(int a)
{
printf("int\n");
}
void func2(int* a)
{
printf("int*\n");
}
int main()
{
func1(0);
func2(NULL);
return 0;
}
// 输出
int
int*
// C++
void func(int a)
{
cout << "int" << endl;
}
void func(int* a)
{
cout << "int*" << endl;
}
int main()
{
func(0);
func(NULL);
return 0;
}
// 输出
int
int
NULL的定义:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0 // C++
#else
#define NULL ((void *)0) // C语言
#endif
#endif
C++中使用nullptr代表指针空值
void func(int a)
{
cout << "int" << endl;
}
void func(int* a)
{
cout << "int*" << endl;
}
int main()
{
func(0);
func(NULL);
func(nullptr);
return 0;
}
// 输出
int
int
int*