1.基础数据类型
计算机中所有数据都是以二进制“0”“1”来表示的,每个叫做一位(bit);计算机可寻址的内存最小单元是8位,也就是一个字节(Byte)
一个字节能表示的最大数是2^8 = 256
sizeof运算符,可以返回某个变量占用的字节数
1.1整型
short至少2字节,16位int至少2字节,而且不能比short短,一般操作系统为32位long至少4字节,而且不能比int短,32位long long至少8字节,而且不能比long短,64位
1.2无符号整型
- 表示正数和
0 - 定义时在类型前加上
unsigned - 在实际应用中使用整型可以只考虑三个原则:
- 一般的整数计算,全部用
int - 如果数值超过了
int的表示范围,用long long - 确定数值不可能为负,用无符号类型
- 一般的整数计算,全部用
1.3char类型
- 常只占一个字节,
8位 - 一般并不用在整数计算,它更重要的用途是表示字符(
character)
ASCII码,它用0~127表示了128个字符,这包括了所有的大小写字母、数字、标点符号、特殊符号以及一些计算机的控制符。
1.4bool类型
- 通常占用
1字节,8位 true就是1,false就是0
1.5浮点类型
- 用来表示小数
- 类型
- 单精度
float通常占用4字节,32位 - 双精度
double通常占用8字节,64位
- 单精度
2.运算符
2.1算数运算符规则
- 一元运算符(正负号)优先级最高;接下来是乘、除和取余;最后是加减
- 算术运算符满足左结合律,也就是说相同优先级的运算符,将从左到右按顺序进行组合
- 算术运算符可以用来处理任意算术类型的数据对象
- 不同类型的数据对象进行计算时,较小的整数类型会被
提升为较大的类型,最终转换成同一类型进行计算 - 对于除法运算
/,执行计算的结果跟操作数的类型有关。如果它的两个操作数(也就是被除数和除数)都是整数,那么得到的结果也只能是整数,小数部分会直接舍弃,这叫整数除法;当至少有一个操作数是浮点数时,结果就会是浮点数,保留小数部分 - 对于取余运算
%(或者叫取模),两个操作数必须是整数类型
2.2赋值运算符
- 用等号
=表示一个赋值 - 赋值规则
- 赋值运算的结果,就是它左侧的运算对象;结果的类型就是左侧运算对象的类型
- 如果赋值运算符两侧对象类型不同,就把右侧的对象转换成左侧对象的类型
C++11新标准提供了一种新的语法:用花括号{}括起来的数值列表,可以作为赋值右侧对象。这样就可以非常方便地对一个数组赋值了- 赋值运算满足右结合律。也就是说可以在一条语句中连续赋值,结合顺序是从右到左
- 赋值运算符优先级较低,一般都会先执行其它运算符,最后做赋值
2.3复合赋值运算符
- 算数运算符
+=-+*=/=%=
- 位运运算符
<<=>>=&=^=|=
2.4递增递减运算符
- 递增
- 递减
- 前置、后置
- 前置时,对象先加
1,再将更新之后的对象值作为结果返回; - 后置时,对象先将原始值作为结果返回,再加
1
- 前置时,对象先加
- 实际应用中,一般都是希望用改变之后的对象值, 所以为了避免混淆,我们通常会统一使用前置的写法。
2.5关系运算符
><==!=>=<=
2.6逻辑运算符
!&&||
2.7条件运算符
条件判断表达式?表达式1:表达式2- 等同于流程控制中的分支语句
if...else...
2.8位运算符
- 直接操作到具体的每一位
bit数据 - 类型
- 移位运算符
- 位逻辑运算符
2.8.1移位运算符
- 左移运算符
<< - 右移运算符
>> - 规则
- 较小的整数类型(
char、short以及bool)会自动提升成int类型再做移位,得到的结果也是int类型 - 左移运算符
<<将操作数左移之后,在右侧补0 - 右移运算符
>>将操作数右移之后,对于无符号数就在左侧补0;对于有符号数的操作则要看运行的机器环境,有可能补符号位,也有可能直接补0 - 由于有符号数右移结果不确定,一般只对无符号数执行位移操作
- 较小的整数类型(
// 181
unsigned char bits = 0xb5;
// 以十六进制显示
cout << hex;
// 0x 0000 02d4
cout << "0xb5 左移2位:" << (bits << 2) << endl; // 0xb5 左移2位:2d4
// 0x 0000 b500
cout << "0xb5 左移8位:" << (bits << 8) << endl; // 0xb5 左移8位:b500
// 0x 8000 0000
cout << "0xb5 左移31位:" << (bits << 31) << endl; // 0xb5 左移31位:80000000
// 0x 0000 0016
cout << "0xb5 右移3位:" << (bits >> 3) << endl; // 0xb5 右移3位:16
cout << dec;
// 乘8操作
cout << (200 << 3) << endl; // 1600
// 除4操作,一般右移是补符号位
cout << (-100 >> 2) << endl; // -25
2.8.2位逻辑运算符
- 计算机存储的每一个“位”
bit都是二进制的,有0和1两种取值 - 按位取反
~,一元运算符,类似逻辑非。对每个位取反值,也就是把1置为0、0置为1 - 位与
&,二元运算符,类似逻辑与。两个数对应位上都为1,结果对应位为1;否则结果对应位为0 - 位或
|,二元运算符,类似逻辑或。两个数对应位上只要有1,结果对应位就为1;如果全为0则结果对应位为0 - 位异或
^,两个数对应位相同,则结果对应位为0;不同则结果对应位为1
// ~ (0... 0000 0101) = 1... 1111 1010
cout << (~5) << endl; // -6
// 0101 & 1100 = 0100
cout << (5 & 12) << endl; // 4
// 0101 | 1100 = 1101
cout << (5 | 12) << endl; // 13
// 0101 & 1100 = 1001
cout << (5 ^ 12) << endl; // 9
3.类型转换
3.1隐式类型转换
- 编译器自动对类型进行转换
- 规则
- 在大多数算术运算中,较小的整数类型(如
bool、char、short)都会转换成int类型。这叫做整数提升;(而对于wchar_t等较大的扩展字符类型,则根据需要转换成int、unsigned int、long、unsigned long、long long、unsigned long long中能容纳它的最小类型) - 当表达式中有整型也有浮点型时,整数值会转换成相应的浮点类型
- 在条件判断语句中,其它整数类型会转换成布尔类型,即
0为false、非0为true - 初始化变量时,初始值转换成变量的类型
- 在赋值语句中,右侧对象的值会转换成左侧对象的类型
- 在大多数算术运算中,较小的整数类型(如
// 整数值赋给bool类型
// b值为true,打印为1
bool b = 25;
// bool类型赋值给算术整型
// s值为0
short s = false;
// 浮点数赋给整数类型
// i值为3
int i = 3.14;
// 整数值赋给浮点类型
// f值为10.0,打印为10
float f = 10;
// 赋值超出整型范围
// us值为0
unsigned short us = 65536;
// s值为-32768
s = 32768;
3.2强制类型转换
- 显式地要求编译器对数据对象的类型进行更改
- 强制类型转换会干扰正常的类型检查,带来很多风险,所以通常要尽量避免使用强制类型转换
// C语言风格
cout << " avg = " << (double) total / num << endl; // avg = 3.33333
// C++函数风格
cout << " avg = " << double (total) / num << endl; // avg = 3.33333
// C++强转运算符
cout << " avg = " << static_cast<double>(total) / num << endl; // avg = 3.33333
4.流程控制语句
C++程序执行的流程结构可以有三种:顺序、分支和循环
4.1条件分支
4.1.1if
- 单分支
- 双分支
- 嵌套分支(多分支)
4.1.2switch
case关键字和后面对应的值,合起来叫做一个“case标签”;case标签必须是一个整型的常量表达式- 任何两个
case标签不能相同 break语句的作用是中断,会直接跳转到switch语句结构的外面- 如果没有
break语句,那么匹配某个case标签之后,程序会从上到下一直执行下去;这会执行多个标签下面的语句,可能发生错误 - 如果没有匹配上任何
case标签的值,程序会执行default标签后面的语句;default是可选的,表示默认要执行的操作
4.1.3循环
C++中的循环语句,有while、do while和for三种
4.1.3.1while
while只需要给定一个判断条件,只要条件为真,就重复地执行语句
4.1.3.2do while
do while和while非常类似,区别在于do while是先执行循环体中的语句,然后再检查条件是否满足。所以do while至少会执行一次循环体
4.1.3.3for
for是用法更加明确的循环语句。它可以把循环变量的定义、循环条件以及循环变量的改变都放在一起,统一声明出来。- 作用
- 初始化语句负责初始化一个变量,这个变量值会随着循环迭代而改变,一般就是
循环变量 - 中间的条件是控制循环执行的关键,为真则执行下面的循环体语句,为假则退出。条件一般会以循环变量作为判断标准
- 最后的表达式会在本次循环完成之后再执行,一般会对循环变量进行更改
- 初始化语句负责初始化一个变量,这个变量值会随着循环迭代而改变,一般就是
4.2跳转
4.2.1break
break语句表示要跳出当前的流程控制语句,它只能出现在switch或者循环语句(while、do while、for)中。当代码中遇到break时,会直接中断距离最近的switch或者循环,跳转到外部继续执行。
4.2.2continue
continue语句表示继续执行循环,也就是中断循环中的本次迭代、并开始执行下一次迭代。很明显,continue只能用在循环语句中,同样针对最近的一层循环有效。
4.2.3goto
goto语句表示无条件地跳转到程序中的另一条语句- 由于
goto可以任意跳转,所以它非常灵活,也非常危险。一般在代码中不要使用goto。
4.2.4return
return是用来终止函数运行并返回结果的。 就是结束主函数并返回结果,一般这句可以省略。
5.复合数据类型
5.1数组
5.1.1数组的定义
- 首先需要声明类型,数组中所有元素必须具有相同的数据类型
- 数组名是一个标识符;后面跟着中括号,里面定义了数组中元素的个数,也就是数组的
长度 - 元素个数也是类型的一部分,所以必须是确定的
// 定义一个数组a1,元素类型为int,个数为10
int a1[10];
const int n = 4;
// 元素个数可以是常量表达式
double a2[n];
// int i = 5;
// 错误,元素个数不能为变量
// int a3[i];
5.1.2数组的初始化
- 对数组做初始化,要使用花括号
{}括起来的数值序列 - 如果做了初始化,数组定义时的元素个数可以省略,编译器可以根据初始化列表自动推断出来
- 初始值的个数,不能超过指定的元素个数
- 初始值的个数,如果小于元素个数,那么会用列表中的值初始化靠前的元素;剩余元素用默认值填充,整型的默认值就是
0 - 如果没有做初始化,数组中元素的值都是未定义的;这一点和普通的局部变量一致
// 正确,初始值说明了元素个数是4
int a3[4] = {1,2,3,4};
// 正确,初始值说明了元素个数是3
float a4[] = {2.5, 3.8, 10.1};
// 正确,指定了前三个元素,其余都为0
short a5[10] = {3,6,9};
// 错误,初始值太多
// long a6[2] = {3,6,9};
// 错误,不能用另一个数组对数组赋值
// int a6[4] = a3;
5.1.3数组的访问
- 数组的下标从
0开始, 因此a[2]访问的并不是数组a的第2个元素,而是第3个元素 - 一个长度为
10的数组,下标范围是0~9,而不是1~10 - 合理的下标,不能小于
0,也不能大于数组长度 - 1, 否则就会出现数组下标越界
5.1.4数组的大小
数组所占空间 = 数据类型所占空间大小 * 元素个数
// a是已定义的数组
int a[4] = {1,2,3,4};
cout << "a所占空间大小:" << sizeof(a) << endl; // a所占空间大小:16
cout << "每个元素所占空间大小:" << sizeof(a[0]) << endl; // 每个元素所占空间大小:4
// 获取数组长度
int aSize = sizeof(a) / sizeof(a[0]);
cout << "数组a的元素个数:" << aSize << endl; // 数组a的元素个数:4
5.1.4多维数组
- 定义
数据类型 数组名*[*行数*][列数] = {数据1, 数据2, 数据3, …};
数据类型 数组名*[*行数*][列数] = {
{数据11, 数据12, 数据13, …},
{数据21, 数据22, 数据23, …},
…
};
- 内嵌的花括号不是必需的,因为数组中的元素在内存中连续存放,可以用一个花括号将所有数据括在一起
- 初始值的个数,可以小于数组定义的长度,其它元素初始化为
0值;这一点对整个二维数组和每一行的一维数组都适用 - 如果省略嵌套的花括号,当初始值个数小于总元素个数时,会按照顺序依次填充(填满第一行,才填第二行);其它元素初始化为
0值 - 多维数组的维度,可以省略第一个,由编译器自动推断;即二维数组可以省略行数,但不能省略列数
// 嵌套的花括号的初始化
int ia[3][4] = {
{1,2,3,4},
{5,6,7,8},
{9,10,11,12}
};
// 只有一层花括号的初始化
int ia2[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
// 部分初始化,其余补0
int ia3[3][4] = {
{1,2,3},
{5,6}
};
// 部分初始化,其余补0
int ia4[3][4] = {1,2,3,4,5,6};
// 省略行数,自动推断
int ia5[][4] = {1,2,3,4,5};
5.2字符串
- 字符串其实就是所谓的“纯文本”,就是各种文字、数字、符号在一起表达的一串信息
5.3结构体
- 结构体是用户自定义的复合数据结构,里面可以包含多个不同类型的数据对象。
struct *结构体名***
{
*类型1 数据对象1*;
*类型2 数据对象2*;
*类型3 数据对象3*;
…
};
5.4枚举
- 枚举类型的定义和结构体非常像,需要使用
enum关键字
// 定义枚举类型
enum week
{
Mon, Tue, Wed, Thu, Fri, Sat, Sun
};
5.5指针
5.5.1指针的定义
- 指针顾名思义,是
指向另外一种数据类型的复合类型。指针是C/C++中一种特殊的数据类型,它所保存的信息,其实是另外一个数据对象在内存中的地址。通过指针可以访问到指向的那个数据对象,所以这是一种间接访问对象的方法。*类型* * *指针变量*;
// p1是指向int类型数据的指针
int* p1;
// p2是指向long类型数据的指针
long* p2;
cout << "p1在内存中长度为:" << sizeof(p1) << endl; // p1在内存中长度为:8
cout << "p2在内存中长度为:" << sizeof(p2) << endl; // p2在内存中长度为:8
- 指针的本质,其实就是一个整数表示的内存地址,它本身在内存中所占大小跟系统环境有关,而跟指向的数据类型无关。
64位编译环境中,指针统一占8字节;若是32位系统则占4字节
5.5.2指针的用法
5.5.2.1获取对象地址给指针赋值
- 指针保存的是数据对象的内存地址,所以可以用地址给指针赋值;获取对象地址的方式是使用
取地址操作符&
int a = 12;
int b = 100;
cout << "a = " << a << endl; // a = 12
cout << "a的地址为:" << &a << endl; // a的地址为:0x7ff7bd58c41c
cout << "b的地址为:" << &b << endl; // b的地址为:0x7ff7bd58c418
// p是指向b的指针
int* p = &b;
// p指向了a
p = &a;
cout << "p = " << p << endl; // p = 0x7ff7bd58c41c
5.5.2.1通过指针访问对象
- 指针指向数据对象后,可以通过指针来访问对象。访问方式是使用
解引用操作符*
int a = 12;
// p是指向a的指针
int *p = &a;
cout << "p指向的内存中,存放的值为:" << *p << endl; // p指向的内存中,存放的值为:12
// 将p所指向的对象(a),修改为25
*p = 25;
cout << "a = " << a << endl; // a = 25
5.5.2.2无效指针、空指针和void*指针
- 无效指针
- 定义一个指针之后,如果不进行初始化,那么它的内容是不确定的(比如
0xcccc)。如果这时把它的内容当成一个地址去访问,就可能访问的是不存在的对象;更可怕的是,如果访问到的是系统核心内存区域,修改其中内容会导致系统崩溃。这样的指针就是无效指针,也被叫做野指针。
- 定义一个指针之后,如果不进行初始化,那么它的内容是不确定的(比如
2.空指针
- 如果先定义了一个指针,但确实还不知道它要指向哪个对象,这时可以把它初始化为
空指针。空指针不指向任何对象。 - 定义方式
- 使用字面值
nullptr,这是C++11引入的方式,推荐使用 - 使用预处理变量
NULL,这是老版本的方式 - 直接使用
0值 - 另外注意,不能直接用整型变量给指针赋值,即使值为
0也不行
- 使用字面值
// 空指针字面值
int *np = nullptr;
// 预处理变量
np = NULL;
// 0值
np = 0;
int zero = 0;
// 错误,int变量不能赋值给指针
// np = zero;
// 输出0地址
cout << "np = " << np << endl; // np = 0x0
// 错误,不能访问0地址的内容
// cout << "*np = " << *np << endl;
- 空指针所保存的其实就是
0值,一般把它叫做0地址;这个地址也是内存中真实存在的,所以也不允许访问 - 空指针一般在程序中用来做判断,看一个指针是否指向了数据对象
3.
void*指针 - 一般来说,指针的类型必须和指向的对象类型匹配,否则就会报错。不过有一种指针比较特殊,可以用来存放任意对象的地址,这种指针的类型是
void* void*指针表示只知道保存了一个地址,至于这个地址对应的数据对象是什么类型并不清楚。所以不能通过void*指针访问对象;一般void*指针只用来比较地址、或者作为函数的输入输出
5.5.3指向指针的指针
- 指针本身也是一个数据对象,也有自己的内存地址。所以可以让一个指针保存另一个指针的地址,这就是
指向指针的指针,有时也叫二级指针;形式上可以用连续两个的星号**来表示。类似地,如果是三级指针就是***,表示指向二级指针的指针。
int i = 1024;
// pi是一个指针,指向int类型的数据
int *pi = &i;
// ppi是一个二级指针,指向一个int* 类型的指针
int **ppi = π
cout << "pi = " << pi << endl; // pi = 0x7ff7b665f41c
cout << "*pi = " << *pi << endl; // *pi = 1024
cout << "ppi = " << ppi << endl; // ppi = 0x7ff7b665f410
cout << "*ppi = " << *ppi << endl; // *ppi = 0x7ff7b665f41c
cout << "**ppi = " << **ppi << endl; // **ppi = 1024
5.5.4指针和const
- 指针可以和
const修饰符结合 - 两种形式:
- 一种是指针指向的是一个常量;
- 另一种是指针本身是一个常量
5.5.4.1指向常量的指针
- 指针指向的是一个常量,所以只能访问数据,不能通过指针对数据进行修改。不过指针本身是变量,可以指向另外的数据对象。这时应该把
const加在类型前。
const int c = 10, c2 = 56;
// 错误,类型不匹配
// int* pc = &c;
// 正确,pc是指向常量的指针,类型为const int *
const int *pc = &c;
// pc可以指向另一个常量
pc = &c2;
int i = 1024;
// pc也可以指向变量
pc = &i;
// 错误,不能通过pc更改数据对象
// *pc = 1000;
5.5.4.2指针常量(const指针)
- 指针本身是一个数据对象,所以也可以区分变量和常量。如果指针本身是一个常量,就意味它保存的地址不能更改,也就是它永远指向同一个对象;而数据对象的内容是可以通过指针改变的。这种指针一般叫做
指针常量。 - 指针常量在定义的时候,需要在星号
*后、标识符前加上const
int i = 10;
int c = 20;
int *const cp = &i;
// 通过指针修改对象的值
*cp = 2048;
cout << "i = " << i << endl;
// 错误,不可以更改cp的指向
// cp = &c;
// ccp是一个指向常量的常量指针
const int *const ccp = &c;
5.5.5指针和数组
5.5.5.1数组名
- 用到数组名时,编译器一般都会把它转换成指针,这个指针就指向数组的第一个元素。所以我们也可以用数组名来给指针赋值。
int arr[] = {1, 2, 3, 4, 5};
cout << "arr = " << arr << endl; // arr = 0x7ff7bf49f400
cout << "&arr[0] = " << &arr[0] << endl; // &arr[0] = 0x7ff7bf49f400
// 可以直接用数组名给指针赋值
int *pia = arr;
// 指针指向的数据,就是arr[0]
cout << "*pia = " << *pia << endl; // * pia = 1
5.5.5.2指针运算
- 如果对
指针pia做加1操作,我们会发现它保存的地址直接加了4,这其实是指向了下一个int类型数据对象
int arr[] = {1, 2, 3, 4, 5};
int *pia = arr;
// pia + 1 指向的是arr[1]
pia + 1;
// 访问 arr[1]
*(pia + 1);
- 所谓的
指针运算,就是直接对一个指针加/减一个整数值,得到的结果仍然是指针。新指针指向的数据元素,跟原指针指向的相比移动了对应个数据单位。
5.5.5.3指针和数组下标
- 我们知道,数组名
arr其实就是指针。这就带来了非常有趣的访问方式:
int arr[] = {1, 2, 3, 4, 5};
// arr[0]
*arr;
// arr[1]
*(arr + 1);
- 这是通过指针来访问数组元素,效果跟使用下标运算符
arr[0]、arr[1]是一样的。进而我们也可以发现,遍历元素所谓的范围for循环,其实就是让指针不停地向后移动依次访问元素。
5.5.5.4指针数组和数组指针
- 指针数组:一个数组,它的所有元素都是相同类型的指针
- 数组指针:一个指针,指向一个数组的指针
int arr[] = {1, 2, 3, 4, 5};
// 指针数组,里面有5个元素,每个元素都是一个int指针
int *pa[5];
// 数组指针,指向一个int数组,数组包含5个元素
int(*ap)[5];
cout << "指针数组pr的大小为:" << sizeof(pa) << endl; // 指针数组pr的大小为:40
cout << "数组指针ap的大小为:" << sizeof(ap) << endl; // 数组指针ap的大小为:8
// pa中第一个元素,指向arr的第一个元素
pa[0] = arr;
// pa中第二个元素,指向arr的第二个元素
pa[1] = arr + 1;
// ap指向了arr整个数组
ap = &arr;
cout << "arr =" << arr << endl; // arr =0x7ff7b091e400
// arr解引用,得到arr[0]
cout << "*arr =" << *arr << endl; // *arr =1
cout << "arr + 1 =" << arr + 1 << endl; // arr + 1 =0x7ff7b091e404
cout << "ap =" << ap << endl; // ap =0x7ff7b091e400
// ap解引用,得到的是arr数组
cout << "*ap =" << *ap << endl; //* ap =0x7ff7b091e400
cout << "ap + 1 =" << ap + 1 << endl; // ap + 1 =0x7ff7b091e414
- 这里可以看到,指向数组
arr的指针ap,其实保存的也是arr第一个元素的地址。arr类型是int *,指向的就是arr[0];而ap类型是int (*)[5],指向的是整个arr数组。所以arr + 1,得到的是arr[1]的地址;而ap + 1,就会跨过整个arr数组
6.引用
6.1引用的用法
- 在做声明时,我们可以在变量名前加上
&符号,表示它是另一个变量的引用。 - 引用必须被初始化。
int a = 10;
// ref是a的引用
int &ref = a;
// 错误,引用必须初始化
// int& ref2;
// ref等于a的值
cout << "ref = " << ref << endl; // ref = 10
cout << "a的地址为:" << &a << endl; // a的地址为:0x7ff7b655e41c
// ref和a的地址完全一样
cout << "ref的地址为:" << &ref << endl; // ref的地址为:0x7ff7b655e41c
- 引用本质上就是一个
别名,它本身不是数据对象,所以本身不会存储数据,而是和初始值绑定(bind)在一起,绑定之后就不能再绑定别的对象了 - 定义了应用之后,对引用做的所有操作,就像直接操作绑定的原始变量一样。所以,引用也是一种间接访问数据对象的方式
int a = 10;
// ref是a的引用
int &ref = a;
// 更改ref相当于更改a
ref = 20;
cout << "a = " << a << endl; // a = 20
int b = 26;
// ref没有绑定b,而是把b的值赋给了ref绑定的a
ref = b;
cout << "a的地址为:" << &a << endl; // a的地址为:0x7ff7be48741c
cout << "b的地址为:" << &b << endl; // b的地址为:0x7ff7be48740c
cout << "ref的地址为:" << &ref << endl; // ref的地址为:0x7ff7be48741c
cout << "a = " << a << endl; // a = 26
- 当然,既然是别名,那么根据这个别名再另起一个别名也是可以的
int a = 10;
// ref是a的引用
int &ref = a;
// 引用的引用
int &rref = ref;
cout << "rref = " << rref << endl; // rref = 10
cout << "a的地址为:" << &a << endl; // a的地址为:0x7ff7b7b4041c
cout << "ref的地址为:" << &ref << endl; // ref的地址为:0x7ff7b7b4041c
cout << "rref的地址为:" << &rref << endl; // rref的地址为:0x7ff7b7b4041c
引用的引用,是把引用作为另一个引用的初始值,其实就是给原来绑定的对象又绑定了一个别名,这两个引用绑定的是同一个对象。- 要注意,引用只能绑定到对象上,而不能跟字面值常量绑定;也就是说,不能把一个字面值直接作为初始值赋给一个引用。而且,引用本身的类型必须跟绑定的对象类型一致。
// 错误,不能创建字面值的引用
// int& ref2 = 10;
double d = 3.14;
// 错误,引用类型和原数据对象类型必须一致
// int& ref3 = d;
6.2对常量的引用
- 可以把引用绑定到一个常量上,这就是
对常量的引用。很显然,对常量的引用是常量的别名,绑定的对象不能修改,所以也不能做赋值操作
const int zero = 0;
// 错误,不能用普通引用去绑定常量
// int& cref = zero;
// 常量的引用
const int &cref = zero;
// 错误,不能对常量赋值
// cref = 10;
- 对常量的引用有时也会直接简称
常量引用。因为引用只是别名,本身不是数据对象;所以这只能代表对一个常量的引用,而不会像常量指针那样引起混淆。 - 常量引用和普通变量的引用不同,它的初始化要求宽松很多,只要是可以转换成它指定类型的所有表达式,都可以用来做初始化
// 正确,可以用字面值常量做初始化
const int &cref2 = 10;
int i = 35;
// 正确,可以用一个变量做初始化
const int &cref3 = i;
double d = 3.14;
// 正确,d会先转成int类型,引用绑定的是一个“临时量”
const int &cref4 = d;
- 这样一来,常量引用和对变量的引用,都可以作为一个变量的
别名,区别在于不能用常量引用去修改对象的值。
int var = 10;
int &r1 = var;
const int &r2 = var;
r1 = 25;
// 错误,不能通过const引用修改对象值
// r2 = 35;
6.3指针和引用
6.3.1引用和指针常量
- 引用的行为,非常类似于
指针常量,也就是只能指向唯一的对象、不能更改的指针。
cout << "a = " << a << endl; // a = 30
cout << "a的地址为:" << &a << endl; // a的地址为:0x7ff7bf06941c
cout << "r = " << r << endl; // r = 30
cout << "r的地址为:" << &r << endl; // r的地址为:0x7ff7bf06941c
cout << "*p = " << *p << endl; // *p = 30
cout << "p = " << p << endl; // p = 0x7ff7bf06941c
- 可以看到,所有用到引用r的地方,都可以用
*p替换;所有需要获取地址&r的地方,也都可以用p替换。这也就是为什么把操作符*,叫做解引用操作符。
6.3.2指针的引用
- 指针本身也是一个数据对象,所以当然也可以给它起别名,用一个引用来绑定它。
int i = 56, j = 28;
// ptr是一个指针,指向int类型对象
int *ptr = &i;
// pref是一个引用,绑定指针ptr
int *&pref = ptr;
// 将指针ptr指向j
pref = &j;
// 将j的值变为20
*pref = 20;
pref是指针ptr的引用,所以下面所有的操作,pref就等同于ptr- 可以有指针的引用、引用的引用,也可以有指向指针的指针;但由于引用只是一个
别名,不是实体对象,所以不存在指向引用的指针
int i = 56;
int &ref = i;
// 错误,不允许使用指向引用的指针
// int&* rptr = &ref;
// 事实上就是指向了i
int *rptr = &ref;
6.3.3引用的本质
- 引用类似于指针常量,但不等同于指针常量。
- 引用的本质,只是
C++引入的一种语法糖,它是对指针的一种伪装。 - 指针是
C语言中最灵活、最强大的特性;引用所能做的,其实指针全都可以做。但是指针同时又令人费解、充满危险性,所以C++中通过引用来代替一些指针的用法。
7.函数
7.1函数定义
- 一个完整的函数定义主要包括以下部分
- 返回类型:调用函数之后,返回结果的数据类型
- 函数名:用来命名代码块的标识符,在当前作用域内唯一
- 参数列表:参数表示函数调用时需要传入的数据,一般叫做
形参, 放在函数名后的小括号里,可以有0个或多个,用逗号隔开 - 函数体:函数要执行的语句块,用花括号括起来
7.2函数调用
- 实参是形参的初始值,所以函数调用时传入实参,相当于执行了
int x = 6的初始化操作;实参的类型必须跟形参类型匹配 - 实参的个数必须跟形参一致;如果有多个形参,要按照位置顺序一一对应
- 如果函数本身没有参数,参数列表可以为空,但空括号不能省
- 形参列表中多个参数用逗号分隔,每个都要带上类型,类型相同也不能省略
- 如果函数不需要返回值,可以定义返回类型为
void - 函数返回类型不能是数组或者函数
7.3参数传递
- 根据形参的类型可以分为两种方式:
- 传值
value - 传引用
reference
- 传值
7.3.1传值参数
- 直接将一个实参的值,拷贝给形参做初始化的传参方式,就被称为
值传递,这样的参数被称为传值参数 - 指针形参
- 如果我们把指向数据对象的指针作为形参,那么初始化时拷贝的就是指针的值;复制之后的指针,依然指向原始数据对象
// 指针形参
void increase(int *p)
{
++(*p);
}
int main()
{
int n = 0;
// 传入n的地址,调用函数后n的值会加1
increase(&n);
}
7.3.2传引用参数
- 采用引用作为函数形参
// 传引用
void increase(int &x)
{
++x;
}
int main()
{
int n = 0;
// 调用函数后n的值会加1
increase(n);
}
- 传引用避免拷贝
// 比较两个字符串的长度
bool isLonger(const string & str1, const string & str2)
{
return str1.size() > str2.size();
}
7.4数组形参
- 数组是不允许做直接拷贝的,所以如果想要把数组作为函数的形参,使用值传递的方式是不可行的。与此同时,数组名可以解析成一个指针,所以可以用传递指针的方式来处理数组
// 指向int类型常量的指针
void printArray(const int*);
void printArray(const int[]);
void printArray(const int[5]);
7.5返回类型
7.5.1无返回值
- 当函数返回类型为void时,表示函数没有返回值
7.5.2有返回值
7.5.3返回数组指针
- 函数也无法直接返回一个数组。同样的,我们可以使用指针或者引用来实现返回数组的目标;通常会返回一个数组指针。
int arr[5] = {1, 2, 3, 4, 5};
// 指针数组,pa是包含5个int指针的数组
int *pa[5];
// 数组指针,ap是一个指针,指向长度为5的int数组
int(*ap)[5] = &arr;
// 函数声明,fun返回值类型为数组指针
int(*fun(int x))[5];
-
解析 -
fun(int x):函数名为fun,形参为int类型的x;(*fun(int x)):函数返回的结果,可以执行解引用操作,说明是一个指针(*fun(int x) )[5]:函数返回结果解引用之后是一个长度为5的数组,说明返回类型是数组指针int ( * fun(int x) )[5]:数组中元素类型为int
-
数组指针的定义比较繁琐,为了简化这个定义,我们可以使用关键字
typedef来定义一个类型的别名
// 类型别名,arrayT代表长度为5的int数组
typedef int arrayT[5];
// fun2的返回类型是指向arrayT的指针
arrayT* fun2(int x);
C++11新标准还提供了另一种简化方式,用一个->符号跟在形参列表后面,再把类型单独提出来放到最后。这种方式叫做尾置返回类型。
// 尾置返回类型
auto fun3(int x) -> int(*)[5];