辜老师的C++课堂笔记

不代表全部内容

第一章 C++引论

教材和参考资料

教材:C++程序设计实践教程(新国标微课版)
出版:华中科技大学出版社
编著:马光志

参考文献 : C++ Primer(第五版)
深度探索C++对象模型
C++ 11标准

1.1程序设计语言

  1. 机器语言: 计算机自身可以识别的语言(CPU指令)
  2. 汇编语言: 接近于机器语言的符号语言(更便于记忆,如MOV指令)
  3. 高级语言: 更接近自然语言的程序设计语言,如ADA、C、PASCAL、FORTRAN、BASIC(面向过程,程序基本单元是函数)
  4. 面向对象的语言:描述对象“特征”及“行为”的程序设计语言,如C++、Java、C#、SMALLTALK等(程序基本单元是类)

1.2程序编译技术

编译过程: 预处理、词法分析、语法分析、代码生成、模块连接。

  1. 预处理:通过#define宏替换和#include插入文件内容生成纯的不包含#define和#include等的C或C++程序。
  2. 词法分析:产生一个程序的单词序列(token)。一个token可以是保留字如if和for、标识符如sin、运算符如+、常量如5和 "abcd"等。
  3. 语法分析:检查程序语法结构,例如if后面是否出现else。
  4. 代码生成:生成低级语言代码如机器语言或汇编语言。C和C++语言的标识符编译为低级语言标识符时会换名,C和C++的换名策略不一样。代码生成的是中间代码(如.OBJ文件)
  5. 模块连接:将中间代码和标准库、非标准库连接起来,形成一个可执行的程序。静态连接是编译时由编译程序完成的连接,动态连接是运行时由操作系统完成的连接。

不同厂家对C++标准的支持程度不一样。一定要确认当前使用的编译器是否支持C++11甚至11以上的标准。

在程序编译过程中,函数调用的实现可以通过静态链接动态链接两种方式来完成。
1. 静态链接
静态链接是在编译时将库的内容与用户程序的目标文件(如 .obj 文件)一起打包,生成最终的可执行文件(如 .exe)。在静态链接中,库文件(如 f.lib)的内容被直接拷贝到可执行文件中。

  1. 工作过程:

用户程序编译后生成的 .obj 文件会和 f.lib 进行链接。
函数 f 的实现会被直接拷贝到用户程序的最终可执行文件中(如 .exe 文件)。
当用户程序启动时,所有使用到的库函数,包括 f,都会被加载到内存中。

  1. 内存占用:

如果有多个程序都使用静态链接,并且都调用了函数 f,那么每个程序的内存中都会有一份 f 函数的副本。这意味着每个程序的内存中都会保存一份独立的 f 函数。

  1. 优点:

不需要在运行时加载库,所有依赖的库函数已经嵌入到可执行文件中,因此不会遇到库缺失的问题。
程序的启动速度较快,因为所有的代码已经在编译时集成。

  1. 缺点:

由于每个程序都包含了库的副本,多个程序会导致内存的重复使用。
如果库函数需要更新,所有使用静态链接的程序都需要重新编译。

  1. 例子:

在静态链接的情况下,假设程序 A 和程序 B 都调用了函数 f,它们的 .exe 文件中都会包含 f 的实现。因此,在内存中,程序 A 和程序 B 各自有一个 f 函数的副本。
2. 动态链接
动态链接是在运行时才将库加载到内存中,并为程序提供所需的函数实现。库文件以动态链接库的形式存在(如 .dll 文件),而不是在编译时嵌入到可执行文件中。

  1. 工作过程:

编译时,用户程序的 .obj 文件和 f.dll 文件进行链接,生成可执行文件。
在这个过程中,目标文件中并不会包含 f 函数的实际代码,而是只包含函数 f 的描述信息。
当程序运行并且调用 f 函数时,系统会动态加载 f.dll,并将 f 函数的实现加载到内存中供程序使用。

  1. 内存占用:

动态链接的最大好处之一是,多个程序可以共享同一个库的副本。也就是说,如果程序 A 和程序 B 都使用 f.dll,那么 f 函数的实现只会在内存中存在一个副本,所有程序共享这一份库。

  1. 优点:

节省内存:多个程序可以共享动态链接库的代码,不会重复加载函数 f 的实现。
易于更新:库的实现可以独立更新,无需重新编译所有依赖它的程序。只需要替换动态链接库的文件即可。

  1. 缺点:

程序启动时可能会稍慢,因为需要在运行时加载库。
运行时依赖动态链接库,若 .dll 文件缺失或损坏,程序将无法正常运行。

  1. 例子:

假设程序 A 和程序 B 都调用 f 函数,并且都通过 f.dll 动态链接。在内存中,程序 A 和程序 B 都会共享同一个 f 函数的副本,而不会各自有独立的副本。这大大减少了内存的重复使用。

第二章 类型、常量及变量

2.1 C++的单词

单词包括常量、变量名、函数名、参数名、类型名、运算符、关键字等。
关键字也被称为保留字,不能用作变量名。
预定义类型如int等也被当作保留字

char16_tchar32_t是C++11引入的两种新的字符类型,用于表示特定大小的Unicode字符
例如 char16_t x = u'马';
wchar_t表示char16_t ,或char32_t
nullptr表示空指针

需要特别注意的是:char可以显示地声明为带符号的和无符号的。因此C++11标准规定char,signed char和unsigned char是三种不同的类型。
但每个具体的编译器实现中,char会表现为signed char和unsigned char中的一种。

unsigned char ua = ~0;
printf("%d  ", ua);//输出255
signed char ub = ~0;
printf("%d  ", ub);//输出-1
char uc = ~0;
printf("%d", uc);//输出-1

2.2 预定义类型(内置数据类型)及值域和常量

2.2.1 常见预定义类型

类型的字节数与硬件、操作系统、编译有关。假定VS2019采用X86编译模式。
void:字节数不定,常表示函数无参或无返回值。

void
是一个可以指向任意类型的指针类型。它本质上是一个
“无类型”*的指针,这意味着它可以指向任何类型的数据,而不关心具体的数据类型。

	int n = 0721;//前置0代表8进制
	double pi = 3.14;
	void* p = &n;
	cout << *(int*)p << endl;//不能直接解引用哦
	p = &pi;	
	cout << *(double*)p << endl;
  • bool:单字节布尔类型,取值false和true。
  • char:单字节有符号字符类型,取值-128~127。
  • short:两字节有符号整数类型,取值-32768~32767。
  • int:四字节有符号整数类型,取值-231~231-1 。
  • long:四字节有符号整数类型,取值-231~231-1 。
  • float:四字节有符号单精度浮点数类型,取值-1038~1038。
  • double:八字节有符号双精度浮点数类型,取值-10308~10308。
    注意:默认一般整数常量当作为int类型,浮点常量当作double类型。
  • char、short、int、long前可加unsigned表示无符号数。
  • long int等价于long;long long占用八字节。
  • 自动类型转换路径(数值表示范围从小到大):
    char→unsigned char→ short→unsigned short→ int→unsigned int→long→unsigned long→float→double→long double。
    数值零自动转换为布尔值false,数值非零转换为布尔值true。
  • 强制类型转换的格式为:
    (类型表达式) 数值表达式
  • 字符常量:‘A’,‘a’,‘9’,‘\’’(单引号),‘\’(斜线),‘\n’(换新行),‘\t’(制表符),‘\b’(退格)
  • 整型常量:9,04,0xA(int); 9U,04U,0xAU(unsigned int); 9L,04L,0xAL(long); 9UL, 04UL,0xAUL(unsigned long), 9LL,04LL,0xALL(long long);
    这里整型常量的类型相信大家看后面的字母也能看出来,例如L代表long,U代表unsigned

2.2.2预定义类型的数值输出格式化

  • double常量:0.9, 3., .3, 2E10, 2.E10, .2E10, -2.5E-10
  • char: %c; short, int: %d; long:%ld; 其中%开始的输出格式符称为占位符
  • 输出无符号数用u代替d(十进制),八进制数用o代替d,十六进制用x代替d
  • 整数表示宽度如printf(“%5c”, ‘A’)打印字符占5格(右对齐)。%-5d表示左对齐。
  • float:%f; double:%lf。float, double:%e科学计数。%g自动选宽度小的e或f。
  • 可对%f或%lf设定宽度和精度及对齐方式。“%-8.2f”表示左对齐、总宽度8(包括符号位和小数部分),其中精度为2位小数。
  • 字符串输出:%s。可设定宽度和对齐:printf(“%5s”,”abc”)。
  • 字符串常量的类型:指向只读字符的指针即const char *, 上述”abc“的类型。
  • 注意strlen(“abc”)=3,但要4个字节存储,最后存储字符‘\0’,表示串结束。

2.3 变量及其类型解析

2.3.1 变量的声明和定义(C++11标准3.1节)

  • 变量说明:描述变量的类型及名称,但没有初始化。可以说明多次
  • 变量定义:描述变量的类型及名称,同时进行初始化。只能定义一次
  • 说明例子:extern int x; extern int x; //变量可以说明多次
  • 定义例子:int x=3; extern int y=4; int z; //全局变量z的初始值为0
  • 模块静态变量:使用static在函数外部定义的变量,只在当前文件(模块)可用。可通过单目::访问。
  • 局部静态变量:使用static在函数内部定义的变量。
static int x, y; //模块静态变量x、y定义,默认初始值均为0
int main( ){    
    static int y;       //局部静态变量y定义, 初始值y=0
    return  ::y+x+y;//分别访问模块静态变量y,模块静态变量x,局部静态变量
} 

为了允许把程序拆分成多个逻辑部分来编写,C++支持分离式编译(separation compilation),即将程序分成多个文件,每个文件独立编译。
为了支持分离式编译,必须将声明(Declaration)定义(Definition)区分开来。声明是使得名字(Identifier,如变量名)为其它程序所知。而定义则负责创建与名字(Identifier)相关联的实体,如为一个变量分配内存单元。因此只有定义才会申请存储空间。
One definition rule(ODR):只能定义一次,但可以多次声明
如果想要声明一个变量而非定义它,就在前面加关键字extern,而且不要显示地初始化变量:

extern int i;   	//变量的声明
int i;//变量的定义(虽没有显示初始化,但会有初始值并分配内存单元存储,即使初始值随机)

任何包含显式初始化的声明成为定义。如果对extern的变量显式初始化,则extern的作用被抵消。
extern int i = 0; //变量的定义
声明和定义的区别非常重要,如果要在多个文件中使用同一个变量,就必须将声明和定义分离,变量的定义必须且只能出现在一个文件中,而其他用到该变量的文件必须对其声明,而不能定义。
例如,头文件里不要放定义,因为头文件可能会被到处include,导致定义多次出现。

值得一提的是C++17引入了 inline 变量,这允许在多个翻译单元中定义同一个全局变量而不引发重复定义的链接错误。inline 变量使得变量像 inline 函数一样,在多个文件中共享同一个定义。而在之前的标准中,inline只能用于函数而不能用于变量

  • 保留字inline用于定义函数外部变量或函数外部静态变量、类内部的静态数据成员。
  • inline函数外部变量的作用域和inline函数外部静态变量一样,都是局限于当前代码文件的,相当于默认加了static。
  • 用inline定义的变量可以使用任意表达式初始化
// header.h
inline int globalVar = 42;

关于其作用,可见C++17之 Inline变量

此外, C++20 引入了模块(Modules)机制,用于替代头文件的传统做法。模块减少了编译依赖并提高了编译速度。在模块中,变量声明和定义的规则更为清晰,模块能够很好地管理变量的可见性和作用范围。
如有兴趣,可见C++20 新特性: modules 及实现现状


2.3.2 变量的初始化(C++11标准8.5节)

  变量在创建时获得一个值,我们说这个变量被初始化了(initialized)。最常见的形式为:类型 变量名=表达式;//表达式的求值结果为变量的初始值
  用=来初始化的形式让人误以为初始化是赋值的一种。
  其实完全不同:初始化的含义是创建变量时设定一个初始值;而赋值的含义是将变量的当前值擦除,而以一个新值来替代。
实际上,C++定义了多种初始化的形式:

int a = 0;
int b = { 0 };
int c(0);
int d{ 0 };

  其中用{ }来初始化作为C++11的新标准一部分得到了全面应用(而以前只在一些场合使用,例如初始化结构变量)。这种初始化形式称为列表初始化(list initialization)

只读变量:使用constconstexpr说明或定义的变量,定义时必须同时初始化。当前程序只能读不能修改其值。constexpr变量必须用编译时可计算表达式初始化。
易变变量:使用volatile说明或定义的变量,可以后初始化。当前程序没有修改其值,但是变量的值变了。不排出其它程序修改。
const实例:extern const int x; const int x=3; //定义必须显式初始化x
volatile例: extern volatile int y; volatile int y; //可不显式初始化y,全局y=0
若y=0,语句if(y==2)是有意义的,因为易变变量y可以变为任何值。

  • volatile 的作用
    编译器通常会进行优化,将变量的值缓存到寄存器中,以提高访问速度。然而,某些情况下,变量的值可能会在程序执行过程中发生外部变化,例如通过硬件、信号、操作系统或多线程访问。因此,需要使用 volatile 关键字告知编译器,每次访问该变量时都要重新读取内存中的值,而不要使用优化的缓存值。

2.3.3 constexpr(c++11)

  **constexpr **是 C++11 引入的关键字,主要用于在编译期计算常量。constexpr 声明的变量或函数保证可以在编译时求值,并且在特定条件下也可以在运行时使用。它用于提高编译期计算的能力,从而优化程序的性能。

  • 任何被声明为 constexpr 的变量或对象,必须在编译时能求出其值。
  • constexpr 变量比 const 更加严格,所有的初始化表达式必须是常量表达式。

字面值类型:对声明constexpr用到的类型必须有限制,这样的类型称为字面值类型(literal type)。
算术类型(字符、布尔值、整型数、浮点数)、引用、指针都是字面值类型
自定义类型(类)都不是字面值类型,因此不能被定义成constexpr
其他的字面值类型包括字面值常量类、枚举
constexpr类型的指针的初始值必须是

  1. nullptr
  2. 0
  3. 指向具有固定地址的对象(全局、局部静态)。

注意局部变量在堆栈里,地址不固定,因此不能被constexpr类型的指针指向
当用constexpr声明或定义一个指针时,constexpr仅对指针有效 ,即指针是const的,

点击查看代码
int i=0;
//constexpr指针
const int *p = nullptr;		//p是一个指向整型常量的指针
constexpr int *q = nullptr;	//q是一个指向整数的constexpr指针
constexpr int *q2 = 0;
constexpr int *q3 = &i; //constexpr指针初始值可以指向全局变量i

int f2() {
    static int ii = 0;
    int jj = 0;
    constexpr int *q4 = &ii;//constexpr指针初始值可以指向局部静态变量
//constexpr int *q5 = &jj;//错误:constexpr指针初始值不可以指向局部变量,局部变量在堆栈,非固定地址
   return ++i;
}

constexpr函数:是指能用于常量表达式的函数,规定:

  • 函数返回类型和形参类型都必须是字面值类型

  • 函数体有且只有一条return语句(c++11)
    C++14之后,constexpr 函数可以包含复杂的控制流语句,如 if、for、while 等,这使得 constexpr 函数更加灵活和强大。

  • constexpr函数被隐式地指定为内联函数,因此函数定义可放头文件

  • constexpr函数体内也可以包括其他非可执行语句,包括空语句,类型别名,using声明

  • 在编译时,编译器会将函数调用替换成其结果值,即这种函数在编译时就求值

    如果传递给 constexpr 函数的参数是常量表达式(可以在编译时求值),那么编译器将会在编译时对这个函数进行求值。
    如果参数不是常量表达式,constexpr 函数将作为普通函数在运行时进行求值。

  • constexpr函数返回的不一定是常量表达式

点击查看代码
//constexpr函数
constexpr int new_size() { return 42; }
constexpr int size = new_size() * 2;  //函数new_size是constexpr函数,因此size是常量表达式

//允许constexpr返回的是非常量
constexpr int scale(int cnt) { return new_size() * cnt;}

//这时scale是否为constexpr取决于实参
//当实参是常量表达式时, scale返回的也是constexpr
constexpr int rtn1 = scale(sizeof(int));  //实参sizeof(int)是常量表达式,因此rtn1也是

int i = 2;
//constexpr int rtn2 = scale(i);  //编译错:实参i不是是常量表达式,因此scale返回的不是常量表达式

2.3.4 复合类型(C++11标准3.9.2)

  复合类型(Compound Type)是指基于其他类型定义的类型,例如指针引用。和内置数据类型变量一样,复合类型也是通过声明语句(declaration)来声明。一条声明语句由基本数据类型和跟在后面的声明符列表(declarator list)组成。每个声明符命名一个变量并指定该变量为与基本数据类型有关的某种类型。
复合类型的声明符基于基本数据类型得到更复杂的类型,如p,&p,分别代表指向基本数据类型变量的指针和指向基本数据类型变量的引用。,&是类型修饰符

在同一条声明语句中,基本数据类型只有一个,但声明符可以有多个且形式可以不同,即一条声明语句中可以声明不同类型的变量:
  int i, *p, &r; //i是int变量,p是int指针,r是int引用
正确理解了上面的定义,就不会对下面的声明语句造成误解:
  int * p1,p2; //p1是int指针,p2不是int指针,而是int变量
为了避免类似的误解,一个好的书写习惯是把类型修饰符和变量名放连一起:
  int *p1,p2, &r1;


2.3.5 指针及其类型理解

  • const默认与左边结合,左边没有东西则与右边结合

  • 指针类型的变量使用*说明和定义,例如:int x=0; int *y=&x;。

  • 指针变量y存放的是变量x的地址,&x表示获取x的地址运算,表示y指向x。

  • 指针变量y涉及两个实体:变量y本身,y指向的变量x。

  • 变量x、y的类型都可以使用const、volatile以及const volatile修饰。
      const int x=3; //不可修改x的值
      const int *y=&x; //可以修改y的值,但是y指向的const int实体不可修改
      const int *const z=&x; //不可修改z的值,且z指向的const int实体也不可改

  • 在一个类型表达式中,先解释优先级高的,若优先级相同,则按结合性解释。
    如:int *y[10][20]; 在y的左边是*,右边是[10],据表2.7知[ ]的优先级更高。
    解释: (1) y是一个10元素数组;(2)每个数组元素均为20元素数组
    (3) 20个元素中的每个元素均为指针int *

  • 但括号()可提高运算符的优先级,如:int (*z)[10][20];
    (…)、[10]、[20]的运算符优先级相同,按照结合性,应依次从左向右解释。
    因此z是一个指针,指向一个int型的二维数组,注意z与y的解释的不同。
    指针移动:y[m][n]+1移动到int 指针指向的下一整数,z+1移动到下一1020整数数组。


指针使用注意事项

  • 只读单元的指针(地址)不能赋给指向可写单元的指针变量。
    例如:
    const int x=3; const int *y=&x; //x是只读单元,y是x的地址
    int z=y; //错:y是指向只读单元的指针
    z=&x; //错:&x是是只读单元的地址
    证明:
    (1)假设int z=&x正确(应用反正法证明)
    (2)由于int z表示z指向的单元可写,故z=5是正确的
    (3)而
    z修改的实际是变量x的值,const int x规定x是不可写的。矛盾。
    可写单元的指针(地址)能赋给指向只读单元的指针变量: y=z;
    前例的const换成volatile或者const volatile,结论一样。
    int
    可以赋值给const int *,const int 不能赋值给int

  • 除了二种例外情况,指针类型类型都要与指向(绑定)的对象严格匹配:二种例外是:

  1. 指向常量的指针(如const int *)可以指向同类型非常量
  2. 父类指针指向子类对象

2.4 引用

引用(reference)为变量起了一个别名,引用类型变量的声明符用&来修饰变量名:

int  i = 10;//i为一个整型变量
int   &r = i; //定义一个引用变量r,引用了变量i,r是i的别名
//定义引用变量时必须马上初始化,即马上指明被引用的变量。
int   &r2;  //编译错误,引用必须初始化,即指定被引用变量

C++11增加了一种新的引用:右值引用(rvalue reference),课本上叫无址引用,用&&定义. 当采用术语引用时,我们约定都是指左值引用(lvalue reference),课本上叫有址引用,用&定义。关于右值引用,会在后续介绍

2.4.1 左值和右值

 C++表达式的求值结果要不是左值(lvaue),要不是右值(rvalue)。在C语言里,左值可以出现在赋值语句的左侧(当然也可以在右侧),右值只能出现在赋值语句的右侧。
 但是在C++中,情况就不是这样。C++中的左值与右值的区别在于是否可以寻址:可以寻址的对象(变量)是左值,不可以寻址的对象(变量)是右值。这里的可以寻址就是指是否可以用&运算符取对象(变量)的地址。

int i = 1;  	//i可以取地址,是左值;1不可以取地址,是右值
//3 = 4;		//错误:3是右值,不能出现在赋值语句左边
const int j = 10; 	//j是左值, j可以取地址
const int *p = &j;
// j = 20;	//错误:const左值(常量左值)不能出现在赋值语句左边

 非常量左值可以出现在赋值运算符的左边,其余的只能出现在右边。右值出现的地方都可以用左值替代。

 区分左值和右值的另一个原则就是:左值持久、右值短暂。左值具有持久的状态(取决于对象的生命周期),而右值要么是字面量,要么是在表达式求值过程中创建的临时对象。

//i++等价于用i作为实参调用下列函数
//第一个参数为引用x,引用实参,因此x = x + 1就是将实参+1;第二个int参数只是告诉编译器是后置++
int operator++(int &x, int)   { 
    int tmp= i; //先取i的值赋给temp
    x = x + 1;
    return tmp; 
}
//因此i++ = 1不成立,因为1是要赋值给函数的返回值,而函数返回后,tmp生命周期已经结束,不能赋值给tmp
//i++等价于operator++(i, int),实参i传递给形参x等价于int &x = i;

2.4.2 引用的本质

引用的本质还是指针,考查以下C++代码及其对应的汇编代码

int i = 10;
int &ri = i;
ri = 20;
//从汇编代码可以看到,引用变量ri里存放的就是i的地址

	int i = 10;
 mov         dword ptr [i],0Ah 	 //将文字常量10送入变量i 

	int &ri = i;
 lea         	eax,[i] 		//将变量i的地址送入寄存器eax
 mov         dword ptr [ri],eax  	//将寄存器的内容(也就是变量i的地址)送入变量ri

	ri = 20;
 mov         eax,dword ptr [ri]  	//将变量ri的值送入寄存器eax
 mov         dword ptr [eax],14h	//将数值20送入以eax的内容为地址的单元中

 引用变量在功能上等于一个常量指针
 但是,为了消除指针操作的风险(例如指针可以++,- -),引用变量ri的地址不能由程序员获取,更不允许改变ri的内容。
 由于引用本质上是常量指针,因此凡是指针能使用的地方,都可以用引用来替代,而且使用引用比指针更安全。例如Java、C#里面就取消了指针,全部用引用替代。

  •  引用与指针的区别是:
  1. 引用在逻辑上是“幽灵”,是不分配物理内存的,因此无法取得引用的地址,也不能定义引用的引用,也不能定义引用类型的数组。引用定义时必须初始化,一旦绑定到一个变量,绑定关系再也不变(常量指针一样)。
  2. 指针是分配物理内存的,可以取指针的地址,可以定义指针的指针(多级指针),指针在定义时无需初始化(但很危险)。对于非常量指针,可以被重新赋值(指向不同的对象,改变绑定关系),可以++, --(有越界风险)
  • 定义了引用后,对引用进行的所有操作实际上都是作用在与之绑定的对象之上。被引用的实体必须是分配内存的实体(能按字节寻址)
  1. 寄存器变量可被引用,因其可被编译为分配内存的自动变量。
  2. 位段成员不能被引用,计算机没有按位编址,而是按字节编址。注意有址引用被编译为指针,存放被引用实体内存地址。
  3. 引用变量不能被引用。对于int x; int &y=x; int &z=y; 并非表示z引用y, int &z=y表示z引用了y所引用的变量i。

例如

点击查看代码

struct A {
	int j : 4;     //j为位段成员
	int k;
} a;
void f() {
	int i = 10;
	int &ri = i;	//引用定义必须初始化,绑定被引用的变量
	ri = 20;		//实际是对i赋值20
	int *p = &ri;	//实际是取i的地址,p指向i,注意这不是取引用ri的地址
	//int &*p = &ri;  	//错误:不能声明指向引用的指针
	//int & &rri = ri;	//错误:不能定义引用的引用
	//int  &s[4];	//错误:数组元素不能为引用类型,否则数组空间逻辑为0

	register int i = 0, &j = i;	//正确:i、j都编译为(基于栈的)自动变量
	int  t[6], (&u)[6] = t;	//正确:有址引用u可引用分配内存的数组t
	int  &v = t[0];	//正确:有址引用变量v可引用分配内存的数组元素
	//int  &w = a.j; 	//错误:位段不能被有址引用,按字节编址才算有内存
	int  &x = a.k; 	//正确:a.k不是位段有内存
}

2.4.3 引用初始化

 引用初始化时,除了二种例外情况,引用类型都要与绑定的对象严格匹配:即必须是用求值结果类型相同的左值表达式来初始化。二种例外是:

  1. const引用
  2. 父类引用绑定到子类对象
点击查看代码
int j = 0;
const int c = 100;
double d = 3.14;
int &rj1 = j;		//用求值结果类型相同的左值表达式来初始化
//int &rj2 = j + 10;	//错误:j + 10是右值表达式
//int &rj3 = c;		//错误:c是左值,但类型是const int,类型不一致
//int &rj4 = j++;		//错误:j++是右值表达式
//int &rd = d;		//错误:d是double类型左值,类型不一致

 而const引用则是万金油,可以用类型相同(如类型不同,看编译器)的左值表达式和右值表达式来初始化

int j = 0;
const int c = 100;
double d = 3.14;
const int &cr1 = j;		//常量引用可以绑定非const左值
const int &cr2 = c;		//常量引用可以绑定const左值
const int &cr3 = j + 10;	//常量引用可以绑定右值
const int &cr4 = d;	//类型不一致,报**警告**错误	(VS2017)

int &&rr = 1;		//rr为右值引用
const int &cr5 = rr;	//常量引用可以绑定同类型右值引用

2.4.3.1 为什么非const引用不能用右值或不同类型的对象初始化?

 对不可寻址的右值或不同类型对象,编译器为了实现引用,必须生成一个临时(如常量)或不同类型的值,对象,引用实际上指向该临时对象,但用户不能通过引用访问。如当我们写
 double dval = 3.14;
 int &ri = dval;
 编译器将其转换成
 int temp = dval; //注意将dval转换成int类型
 int &ri = temp;
 如果我们给ri赋给新值,改变的是temp而不是dval。对用户来说,感觉赋值没有生效(这不是好事)。
 const引用不会暴露这个问题,因为它本来就是只读的。
 干脆禁止用右值或不同类型的变量来初始化非const引用比“允许这样做,但实际上不会生效”的方案好得多。

2.4.3.2 &定义的有址引用(左值引用)

 const和volatile有关指针的用法可推广至&定义的(左值)引用变量
 例如:“只读单元的指针(地址)不能赋给指向可写单元值的指针变量”推广至引用为“只读单元的引用不能初始化引用可写单元的引用变量”。如前所述,反之是成立的。
 int &可以赋值给const int &,const int &不能赋值给int &

  1.       const int &u=3;   //u是只读单元的引用
    
  2.       int &v=u;	          //错:u不能初始化引用可写单元的引用变量v
    
  3.       int x=3; int &y=x;//对:可进行y=4,则x=4。 
    
  4.       const int &z=y;    //对:不可进行z=4。但若y=5,则x=5, z=5。
    
  5.       volatile int &m=y;//对,m引用x。
    

2.4.3.3 &&定义的无址引用(右值引用)

右值引用:就是必须绑定到右值的引用。
 右值引用的重要性质:只能绑定到即将销毁的对象,包括字面量,表达式求值过程中创建的临时对象。
 返回非引用类型的函数、算术运算、布尔运算、位运算、后置++,后置--都生成右值,右值引用和const左值引用可以绑定到这些运算的结果上。
 c++ 11中的右值引用使用的修饰符是&&,如:
 int &&aa = 1; //实质上就是将不具名(匿名)变量取了个别名
 aa = 2; //可以。匿名变量1的生命周期本来应该在语句结束后马上结束,但是由于被右值引用变量引用,其生命期将与右值引用类型变量aa的生命期一样。这里aa的类型是右值引用类型(int &&),但是如果从左值和右值的角度区分它,它实际上是个左值

  1. &&定义右值引用变量,必须引用右值。如int &&x=2;
  2. 注意,以上x是右值引用(引用了右值),但它本身是左值,即可进行赋值:x=3;
  3. 但:const int &&y=2;//不可赋值: y=3;
  4. 同理:“右值引用共享被引用对象的“缓存”,本身不分配内存。”
int &&  *p;	//错:p不能指向没有内存的无址引用
int &&  &q;	//错:int &&没有内存,不能被q引用
int &  &&r;	//错:int &没有内存,不能被r引用。
int &&  &&s;	//错:int &&没有内存,不能被s引用
int &&t[4];    	//错:数组的元素不能为int &&:数组内存空间为0。
const int a[3]={1,2,3};   int(&& t)[3]=a; //错:a是有址的, 有名的均是有址的。&&不能引用有址的 
int(&& u)[3]= {1,2,3};    //正确,{1,2,3}是无址右值

右值引用的主要作用:

  1. 移动语义:允许临时对象的资源(如内存、文件句柄等)被“移动”到另一个对象中,而不是进行拷贝。这可以避免不必要的资源复制,提高程序效率。

  2. 完美转发:在模板编程中,能够将函数的参数以左值或右值的形式完美转发给另一个函数。

int b = 1;
//int && c = b; //编译错误! 右值引用不能引用左值

A getTemp() { return A( ); }
A o = getTemp();   // o是左值  getTemp()的返回值是右值(临时变量),被拷贝给o,会引起对象的拷贝

// getTemp()返回的右值本来在表达式语句结束后,其生命也就该终结了,而通过右值引用,该右值又重获新生,其生命期将与右值引用类型变量refO的生命期一样,只要refO还活着,该右值临时变量将会一直存活下去。
A && refO = getTemp();   //getTemp()的返回值是右值(临时变量),可以用右值引用,但不会引起对象的拷贝

//注意:这里refO的类型是右值引用类型(A &&),但是如果从左值和右值的角度区分它,它实际上是个左值(其生命周期取决于refO)。因为可以对它取地址,而且它还有名字,是一个已经命名的左值。因此
A *p = &refO;
//不能将一个右值引用绑定到一个右值引用类型的变量上
//A &&refOther = refO; //编译错误,refO是左值
  • 若函数不返回(左值)引用类型,则该函数调用的返回值是无址(右值)的
int &&x=printf(“abcdefg”);  //对:printf( )返回无址右值
     	int &&a=2;	//对:引用无址右值
     	int &&b=a; 	//错:a是有名有址的,a是左值
     	int&& f( ) { return 2; }
int &&c=f( );   	//对:f返回的是无址引用,是无址的
  • 位段成员是无址的。
struct A {   int a;	/*普通成员:有址*/     int b : 3; /*位段成员:无址*/ }p = { 1,2 };
int &&q=p.a;	//错:不能引用有址的变量,p.a是左值
int &&r=p.b;	//对:引用无址左值

2.5 枚举、数组

2.5.1 枚举

  • 枚举一般被编译为整型,而枚举元素有相应的整型常量值;
  • 第一个枚举元素的值默认为0,后一个元素的值默认在前一个基础上加1
    enum WEEKDAY {Sun, Mon, Tue, Wed, Thu, Fri, Sat}; //Sun=0, mon=1
    WEEKDAY w1=Sun, w2(Mon); //可用限定名WEEKDAY::Sun,
  • 也可以为枚举元素指定值,哪怕是重复的整数值。
    enum E{e=1, s, w= –1, n, p}; //正确, s=2, p= 1和e相等
  • 如果使用“enum class”(C++11 引入)或者“enum struct”定义枚举类型,则其元素必须使用类型名限定元素名
    enum struct RND{e=2, f=0, g, h}; //正确:e=2,f=0,g=1,h= 2
    RND m= RND::h; //必须用限定名RND::h
    int n=sizeof(RND::h); //n=4, 枚举元素实现为整数

2.5.2 元素、下标及数组

  1. 数组元素按行存储, 对于“int a[2][3]={{1,2,3},{4,5,6}};”,先存第1行再存第2行
    a: 1, 2, 3, 4, 5, 6 //第1个元素为a[0][0], 第2个为a[0][1],第4个为a[1][0]```

  2. 若上述a为全局变量,则a在数据段分配内存,1,2…6等初始值存放于该内存。

  3. 若上述a为静态变量,则a的内存分配及初始化值存放情况同上。

  4. 若上述a函数内定义的局部非静态变量,则a的内存在栈段分配

  5. C++数组并不存放每维的长度信息,因此也没有办法自动实现下标越界判断。每维下标的起始值默认为0。

  6. 数组名a代表数组的首地址,其代表的类型为int [2][3]或int(*)[3]。

  7. 一维数组可看作单重指针,反之也成立。例如:
    int b[3]; //*(b+1)等价于访问b[1]
    int p=&b[0]; //(p+2)等价访问p[2],也即访问b[2]

  8. 字符串常量可看做以’\0’结束存储的字符数组。例如“abc”的存储为
    | ‘a’ | ‘b’ | ‘c’ | ‘\0’ | //字符串长度即strlen(“abc”)=3,但需要4个字节存储。
    char c[6]=“abc”; //sizeof(c)=6,strlen(c)=3, “abc”可看作字符数组
    char d[ ]=“abc”; //sizeof(d)=4,编译自动计算数组的大小, strlen(d)=3
    const char*p=“abc”;//sieof(p)=4, p[0]=‘a’,“abc”看作const char指针,注意必须加const

  9. 故可以写: cout << “abc”[1]; //输出b

2.6 运算符及表达式

C++运算符、优先级、结合性见表。优先级高的先计算,相同时按结合性规定的计算顺序计算。可分如下几类:

  1. 位运算:按位与&、按位或|、按位异或^、左移、右移。左移1位相当于乘于2,右移1位相当于除于2。
  2. 算数运算:加+、减-、乘*、除/、模%。
  3. 关系运算:大于、大等于、等于、小于、小等于
  4. 逻辑运算:逻辑与&&、逻辑或||
优先级 运算符 描述 结合性 运算类型
1 :: 作用域解析运算符 其他
2 ++ -- 后缀自增、自减 从左到右 算数运算
2 () 函数调用 从左到右 其他
2 [] 下标 从左到右 其他
2 . -> 成员访问 从左到右 其他
2 typeid 类型信息 从左到右 其他
2 const_cast dynamic_cast reinterpret_cast static_cast 类型转换 从右到左 其他
3 ++ -- 前缀自增、自减 从右到左 算数运算
3 + - 正负号 从右到左 算数运算
3 ! ~ 逻辑非、按位取反 从右到左 逻辑运算、位运算
3 * & 指针解引用、取地址 从右到左 指针运算
3 sizeof 取大小 从右到左 其他
3 new new[] 动态内存分配 从右到左 其他
3 delete delete[] 动态内存释放 从右到左 其他
3 typeid 类型信息 从右到左 其他
3 decltype 推导类型 其他
4 .* ->* 成员指针运算符 从左到右 指针运算
5 * / % 乘法、除法、取余 从左到右 算数运算
6 + - 加法、减法 从左到右 算数运算
7 << >> 位移 从左到右 位运算
8 < <= > >= 比较运算符 从左到右 关系运算
9 == != 等于、不等于 从左到右 关系运算
10 & 按位与 从左到右 位运算
11 ^ 按位异或 从左到右 位运算
12 ` ` 按位或 从左到右
13 && 逻辑与 从左到右 逻辑运算
14 ` ` 逻辑或
15 ?: 条件运算符 从右到左 三元运算符
16 = 赋值 从右到左 赋值运算
16 += -= *= /= %= <<= >>= &= ^= ` =` 复合赋值 从右到左
16 throw 异常抛出 从右到左 其他
17 , 逗号运算符 从左到右 其他
  • 由于C++逻辑值可以自动转换为整数0或1,因此,数学表达式的关系运算在转换为C++表达式容易混淆整数值和逻辑值。假如x=3,则数学表达式 “1<x<2”的结果为假,但若C++计算则1<x<2⇔1<3<2⇔1<2⇔真,

  • 数学表达式实际上是两个关系运算的逻辑与,相当于C++的“1<x&&x<2”。

  • 赋值表达式也是C++一种表达式。对于int x(2); x=x+3; 赋值语句中的表达式:
    x+3是加法运算表达式,其计算结果为传统右值5。
    x=5是赋值运算表达式,其计算结果为传统左值x(x的值为5) 。
    由于计算结果为传统左值x,故还可对x赋值7,相当于运算:(x=x+3)=7;结果为左值x

  • 选择运算使用”?:”构成, 例如:y=(x>0)?1:0; 翻译成等价的C++语句如下。
    if(x>0) y=1;
    else y=0;

  • 前置运算“++c”、后置运算“c ++”为自增运算;相当于c=c+1,前置运算“—c”、后置运算“c –”为自减运算,相当于c=c-1。前置运算先运算后取值,结果为传统左值;后置运算先取值后运算,结果为传统右值。

posted @ 2024-09-21 00:24  Losyi  阅读(65)  评论(0编辑  收藏  举报