前端学习C++基础知识记录-流水账

146 阅读30分钟

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就是1false就是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移位运算符

  • 左移运算符<<
  • 右移运算符>>
  • 规则
    • 较小的整数类型(charshort以及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都是二进制的,有01两种取值
  • 按位取反~,一元运算符,类似逻辑非。对每个位取反值,也就是把1置为00置为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隐式类型转换

  • 编译器自动对类型进行转换
  • 规则
    • 在大多数算术运算中,较小的整数类型(如boolcharshort)都会转换成int类型。这叫做整数提升;(而对于wchar_t等较大的扩展字符类型,则根据需要转换成intunsigned intlongunsigned longlong longunsigned long long中能容纳它的最小类型)
    • 当表达式中有整型也有浮点型时,整数值会转换成相应的浮点类型
    • 在条件判断语句中,其它整数类型会转换成布尔类型,即0false非0true
    • 初始化变量时,初始值转换成变量的类型
    • 在赋值语句中,右侧对象的值会转换成左侧对象的类型
// 整数值赋给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 whilewhile非常类似,区别在于do while是先执行循环体中的语句,然后再检查条件是否满足。所以do while至少会执行一次循环体
4.1.3.3for
  • for是用法更加明确的循环语句。它可以把循环变量的定义、循环条件以及循环变量的改变都放在一起,统一声明出来。
  • 作用
    • 初始化语句负责初始化一个变量,这个变量值会随着循环迭代而改变,一般就是循环变量
    • 中间的条件是控制循环执行的关键,为真则执行下面的循环体语句,为假则退出。条件一般会以循环变量作为判断标准
    • 最后的表达式会在本次循环完成之后再执行,一般会对循环变量进行更改

4.2跳转

4.2.1break

  • break语句表示要跳出当前的流程控制语句,它只能出现在switch或者循环语句(whiledo whilefor)中。当代码中遇到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*指针
  1. 无效指针
    • 定义一个指针之后,如果不进行初始化,那么它的内容是不确定的(比如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 = &pi;

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];