辜老师的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 
  • 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个字节存储,最后存储字符‘’,表示串结束。

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 = ⅈ//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。
    

左值引用的主要作用:

  1. 为对象创建别名
    左值引用允许为一个对象创建别名,方便代码编写和对象操作。
  2. 作为函数参数传递
    左值引用可以用于函数参数的传递,尤其是在函数需要修改参数时。相比值传递,引用传递可以避免复制对象,节省内存和提高性能。
  3. 避免不必要的拷贝
    左值引用避免了传值带来的对象拷贝开销,尤其在传递大型对象时,引用可以显著提高效率。
void print(const std::string& str) {
    std::cout 
  1. 允许修改引用的对象
    通过左值引用,可以直接修改所引用对象的内容。这种特性使得它适用于修改对象的场景,例如更新数据或改变状态。
  2. 在返回值中使用左值引用
    函数可以返回左值引用,允许调用者直接操作函数返回的对象。注意,返回的对象必须有较长的生命周期,不能返回局部变量的引用。
int& getX(int& x) {
    return x;  // 返回 x 的引用
}

int main() {
    int a = 5;
    getX(a) = 100;  // 可以通过函数返回的引用修改 a
    std::cout 
  1. 支持左值对象的传递
    左值引用可以绑定到左值对象,而左值是指那些在内存中具有明确地址、生命周期较长的对象。左值引用允许对这些左值进行操作。
int x = 10;
int& ref = x;  // 左值引用绑定到 x
ref = 20;      // 修改 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. 字符串常量可看做以’’结束存储的字符数组。例如“abc”的存储为
    | ‘a’ | ‘b’ | ‘c’ | ‘’ | //字符串长度即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 //输出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

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

  • 赋值表达式也是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。前置运算先运算后取值,结果为传统左值;后置运算先取值后运算,结果为传统右值。


第三章 语句、函数及程序设计

3.1 C++的语句

  1. 语句是用于完成函数功能的基本命令。
  2. 语句包括空语句、值表达式语句、复合语句、if语句、switch语句、for语句、while语句、do语句、break语句、continue语句、标号语句、goto语句等。
  3. 空语句:仅由分号“;”构成的语句。
  4. 值表达式语句:由数值表达式(求值结果为数值)加上分号“;”构成的语句。例如:x=1; y=2;
  5. 复合语句:复合语句是由 “{ }“括起的若干语句。例如:
  6. if语句:也称分支语句,根据满足的条件转向不同的分支。两种形式:
    1. if(x>1) y=3; //单分支:当x>1时,使y=3
    2. if(x>1) y=3; else y=4; //双分支:当x>1时,使y=3,否则使y=4
      上述if语句红色部分可以是任何语句,包括新的if语句:称之为嵌套的if语句。
  7. switch语句:也称多路分支语句,可提供比if更多的分支。
    1. 括号中的expression只能是
    2. 小于等于int的类型包括枚举。
    3. 进入swtich的“{ }”, 可以定义新的类型和局部变量
    4. bool, char, short, int等值均可。
    5. “default”可出现在任何位置。
    6. 未在case中的值均匹配“default”。
    7. 若当前case的语句没有break,则继续执行下一个case直到遇到break或结束。
    8. switch的”( )”及“{ }”中均可定义变量,但必须先初始化再被访问。

3.2 C++的函数

3.2.1 函数

  1. 函数用于“分而治之”的软件设计,以将大的程序分解为小的模块或任务。
  2. 函数说明(声明)不定义函数体,函数定义必须定义函数体。说明可多次,定义仅能实施一次。
  3. 函数可说明或定义为四种作用域:(1)全局函数(默认);(2)内联即inline函数;(3)外部即extern函数;(4)静态即static函数
  4. 全局函数可被任何程序文件(.cpp)的程序用,只有全局main函数不可被调用(新标准)。故它是全局作用域的。
  5. 内联函数可在程序文件内或类内说明或定义,只能被当前程序文件的程序调用。它是局部文件作用域的,可被编译优化(掉)。
  6. 静态函数可在程序文件内或类内说明或定义。类内的静态函数不是局部文件作用域的,程序文件内的静态函数是局部文件作用域的。
  7. 函数说明(声明)可以进行多次,但定义只能在某个程序文件(.cpp)进行一次
点击查看代码
  int d( ) { return 0; }		//默认定义全局函数d:有函数体
            extern int e(int x);		//说明函数e:无函数体。可以先说明再定义,且可以说明多次
            extern int e(int x) { return x; }	//定义全局函数e:有函数体
            inline void f( ) { } //定义程序文件局部作用域函数f:有函数体,可优化,内联。仅当前程序文件可调用
            void g( ) { }	       //定义全局函数g:有函数体,无优化。
            static void h( ){ } //定义程序文件局部作用域函数h:有函数体,无优化,静态。仅当前程序文件可调用
            void main(void) {	
	extern int d( ),   e(int);   //说明要使用外部函数:d, e均来自于全局函数。可以说明多次。
	extern void f( ),   g( );    //说明要使用外部函数:f来自于局部函数(inline), g来自于全局函数
	extern void h( );	      //说明要使用外部函数:h来自于局部函数(static)
            }

3.2.2 main函数

  1. 主函数main是程序入口,它接受来自操作系统的参数(命令行参数)。
  2. main可以返回0给操作系统,表示程序正常执行,返回其它值表示异常。
  3. main的定义格式为int main(int argc, char*argv[ ]),argc表示参数个数,argv存储若干个参数。
  4. 函数必须先说明或定义才能调用,如果有标准库函数则可以通过#include说明要使用的库函数的参数。
  5. 例如在stdio.h中声明了scanf和printf函数,分别返回成功输入的变量个数以及成功打印的字符个数:int printf(const char*, …);
  6. 例如在string.h中声明了strlen函数,返回字符串s的长度(不包括字符串借宿标志字符‘’):int strlen(const char *s);
  7. 注意在#include 之前,必须使用
    #define _CRT_SECURE_NO_WARNINGS

3.2.3 函数缺省参数

省略参数…表示可以接受0至任意个任意类型的参数。通常须提供一个参数表示省略了多少个实参。

long sum(int n, ...) {
	      long s = 0; int* p = &n + 1;    //p指向第1个省略参数
	      for (int k = 0; k 

注意:参数n和省略参数连续存放,故可通过&n+1得到第1个省略参数的地址;若省略参数为double类型,则应进行强制类型转换。且必须在x86下运行,x64会优先使用寄存器而不是栈导致无法寻址
double *p=(double *)(&n+1);

C++ 提供了标准的机制来安全、正确地处理可变参数。这是通过使用 stdarg.h 或 cstdarg 中的相关宏来实现的。以下是使用标准宏处理可变参数的正确方式:

#include 
#include 

long sum(int n, ...) {
    long s = 0;
    va_list args;         // 声明可变参数列表
    va_start(args, n);    // 初始化列表,args 指向第一个可变参数
    for (int k = 0; k 

3.3 作用域

3.3.1 作用域的种类

  1. 程序可由若干代码文件(.cpp)构成,整个程序为全局作用域:全局变量和函数属于此作用域。
  2. 稍小的作用域是代码当前文件作用域:函数外的static变量和函数属此作用域。
  3. 更小的作用域是函数体:函数局部变量和函数参数属于此作用域。
  4. 在函数体内又有更小的复合语句块作用域。
  5. 最小的作用域是数值表达式:常量在此作用域。
  6. 除全局作用域外,同层不同作用域可以定义同名的常量、变量、函数。但他们为不同的实体。
  7. 如果变量和常量是对象,则进入面向对象的作用域。
  8. 同名变量、函数的作用域越小、被访问的优先级越高

3.3.2 名字的作用域

 每个名字(标识符:由程序员命名的名字)都会指向一个实体:变量、函数、类型等。
 但同一个名字处于不同的位置,也可能指向不同的实体,这是由名字的作用域(scope)决定的。C++绝大多数作用域用{ }分割。

  1. 定义于所有花括号之外名字具有全局作用域,在整个程序范围内可见;
  2. 定义于{ }之内的名字,其作用域始于名字的声明语句,结束于所在的{}的最后一条语句;这样的名字的作用域是块作用域(block scope);
  3. 作用域可以嵌套。作用域一旦申明了一个名字,它所嵌套的所有作用域都能访问该名字,同时允许在内部嵌套作用域内定义同名名字。
    1. 作用域越小,访问优先级越高;因此内部嵌套作用域内的名字会隐藏外层作用域里同名名字;
    2. 函数的形参相当于函数里定义的局部变量,其作用域是函数体;
#include 

int gi = 1; //i具有全局作用域

void func(){
	 //具有全局作用域的变量gi到处可以访问,输出0
	std::cout 

3.4 生命期

  1. 作用域是变量等可使用的空间,生命期是变量等存在的时间。
  2. 变量的生命期从其被运行到的位置开始,直到其生命结束(如被析构或函数返回等)为止。
  3. 常量的生命期即其所在表达式。
  4. 函数参数或自动变量的生命期当退出其作用域时结束。
  5. 静态变量的生命期从其被运行到的位置开始,直到整个程序结束。
  6. 全局变量的生命期从其初始化位置开始,直到整个程序结束。
  7. 通过new产生的对象如果不delete,则永远生存(内存泄漏)。
  8. d(包括函数参数),否则导致变量的值不确定:因为内存变量的生命已经结束(内存已做他用)。
#include 

//返回内层作用域的自动变量
int & g() {
	int x = 10;
	return x;
}
int main()
{
	int & r = g();  //外层作用域引用变量r引用了g里面的自动变量
	std::cout 

第四章 C++的类

4.1 类的声明和定义

类保留字:class、struct或union可用来声明和定义类。

class 类型名;//前向声明
class 类型名{//类的定义
private:
    私有成员声明或定义;
protected:
    保护成员声明或定义;
public:
    公有成员声明或定义;
};

image

4.1.1 定义类时应注意的问题:

  1. 使用private、protected和public保留字标识主体中每一区间的访问权限,同一保留字可以多次出现;
  2. 同一区间内可以有数据成员、函数成员和类型成员,习惯上按类型成员、数据成员和函数成员分开;
  3. 成员在类定义体中出现的顺序可以任意,函数成员的实现既可以放在类的外面,也可以内嵌在类定义体中(此时会自动成为内联函数);但是数据成员的声明/定义顺序与初始化顺序有关(先声明/定义的先初始化)。
  4. 若函数成员在类定义体外实现,则在函数返回类型和函数名之间,应使用类名和作用域运算符“::”来指明该函数成员所属的类。
  5. 类的定义体花括号后要有分号作为定义体结束标志。
  6. 在类定义体中允许对数据成员定义默认值,若在构造函数的参数列表后面的“:”和函数体的“{”之间对其进行了初始化(在成员初始化列表进行初始化),则默认值无效,否则用默认值初始化;

4.1.2 构造函数和析构函数

  1. 构造函数和析构函数:是类封装的两个特殊函数成员,都有固定类型的隐含参数this(对类A,this指针为A* const this) 。
  2. 构造函数:函数名和类名相同的函数成员。可在参数表显式定义参数,通过参数变化实现重载
  3. 析构函数:函数名和类名相同且带波浪线的参数表无参函数成员。故无法通过参数变化重载析构函数
  4. 定义变量或其生命期开始时自动调用构造函数,生命期结束时自动调用析构函数。
  5. 同一个对象仅自动构造一次。构造函数是唯一不能被显式(人工,非自动)调用的函数成员
  6. 构造函数和析构函数都不能定义返回类型。
  7. 如果类没有自定义的构造函数和析构函数,则C++为类生成默认的构造函数(不用给实参的构造函数)和析构函数。
  8. 构造函数的参数表可以出现参数,因此可以重载
  9. 构造函数用来为对象申请各种资源,并初始化对象的数据成员。构造函数有隐含参数this,可以在参数表定义若干参数,用于初始化数据成员。
  10. 析构函数是用来毁灭对象的,析构过程是构造过程的逆过程。析构函数释放对象申请的所有资源。
  11. 析构函数既能被显式调用,也能被隐式(自动)调用。由于只有一个固定类型的this,故不可能重载,只能有一个析构函数。
  12. 若实例数据成员有指针指向malloc/new的内存,应当防止反复析构(用指针是否为空做标志)。
  13. 联合也是类,可定义构造、析构以及其它函数成员。

4.1.3 不同对象的构造和析构

 构造函数在自动变量 (对象)被实例化或通过new产生对象时被自动调用一次,是唯一不能被显式调用的函数成员。
 析构函数在变量 (对象)的生命期结束时被自动调用一次,通过new产生的对象需要用delete手动释放(自动调用析构)。由于析构函数可被显式反复调用,而有些资源是不能反复析构的,例如不能反复关闭文件,因此,必要时要防止对象反复释放资源。

  1. 全局变量 (对象) :main执行之前由开工函数调用构造函数,main执行之后由收工函数调用析构函数。
  2. 局部自动对象 (非static变量) :在当前函数内对象实例化时自动调用构造函数, 在当前函数正常返回时自动调用析构函数。
  3. 局部静态对象 (static变量) :定义时自动调用构造函数, main执行之后由收工函数调用析构函数。
  4. 常量对象:在当前表达式语句定义时自动调用构造函数,语句结束时自动调用析构函数
点击查看代码
/全局对象,mian函数之前被开工函数构造,由收工函数自动析构,全局对象在数据段里
MYSTRING  gs(“Global String”); 

void f(){
	//局部自动变量,函数返回后自动析构,局部对象在堆栈里
	 MYSTRING x(“Auto local variable”); 

	//静态局部变量,由收工函数自动析构,静态局部对象在数据段里
	static MYSTRING y(“Static local variable”); 
	
	//new出来的对象在堆里(Heap)
	 MYSTRING *p = new MYSTRING(“ABC”); 	
	delete(p); //delete new出来的对象的析构是程序员的责任,delete会自动调用析构函数
}

C++11新标准中出现了智能指针的概念,就是要减轻程序员的必须承担的手动delete动态分配的内存(new出来的东西)的负担,后面会介绍。

析构函数要防止反复释放资源

点击查看代码
#include 
#include   // 使用 C++ 中的字符串库
#include 
#include 
using namespace std;
struct MYSTRING {
    char* s;
    MYSTRING(const char*);
    ~MYSTRING();
};
MYSTRING::MYSTRING(const char* t) {
    s = (char*)malloc(strlen(t) + 1);
    strcpy(s, t);
    cout 

程序不同结束形式对对象的影响:

  1. exit退出:局部自动对象不能自动执行析构函数,故此类对象资源不能被释放。静态和全局对象在exit退出main时自动执行收工函数析构。
  2. abort退出:所有对象自动调用的析构函数都不能执行。局部和全局对象的资源都不能被释放,即abort退出main后不执行收工函数。
  3. return返回:隐式调用的析构函数得以执行。局部和全局对象的资源被释放。
    int main( ){ …; if (error) return 1; …;}
    提倡使用return。如果用abort和exit,则要显式调用析构函数。另外,使用异常处理时,自动调用的析构函数都会执行。
点击查看代码
//例4.2本例说明exit和abort的正确使用方法。
#include 
#include “string.cpp”  	//不提倡这样include:因为string.cpp内有函数定义
STRING x("global");	//自动调用构造函数初始化x
void main(void){
      short error=0;
      STRING y("local");	//自动调用构造函数初始化y
      switch(error) {
      case  0: return;  	//正常返回时自动析构x、y
      case  1: y.~STRING( );	//为防内存泄漏,exit退出前必须显式析构y
            	    exit(1);		    
      default: x.~STRING( );	//为防内存泄漏,abort退出前须显式析构x、y
	    y.~STRING( ); 
	    abort( );
       }
}

接受与删除编译自动生成的函数:default接受, delete:删除。

点击查看代码
//例4.4使用delete禁止构造函数以及default接受构造函数。
struct A {
    int x=0;
    A( ) = delete;		//删除由编译器产生构造函数A( )
    A(int m): x(m) { }
    A(const A&a) = default;	//接受编译生成的拷贝构造函数A(const A&)
};
void main(void) {
    A x(2);			//调用程序员自定义的单参构造函数A(int)
    A y(x);			//调用编译生成的拷贝构造函数A(const A&)
    //A u;			//错误:u要调用构造函数A( ),但A( )被删除	
    A v( );			//正确:声明外部无参非成员函数v,且返回类型为A
}//“A v( );”等价于“extern  A v( );”

4.2 成员访问权限及其访问

4.3 内联、匿名类及位段

4.4 new和delete

4.5 隐含参数this

4.6 对象的构造与析构

4.6.1 非静态数据成员初始化

  • 类的 非静态数据成员可以在如下位置初始化:
  1. 类体中进行就地初始化(称为就地初始化)(=或{}都可以),但成员初始化列表的效果优于就地初始化。
  2. 成员初始化列表初始化
  3. 可以同时就地初始化和在成员初始化列表里初始化,当二者同时使用时,并不冲突,初始化列表发生在就地初始化之后,即最终的初始化结果以初始化列表为准
  4. 在构造函数体内严格来说不叫初始化,叫赋值

 在下面的例子中,必须自己定义构造函数,没有自定义构造函数何来成员初始化列表。构造函数是否带参数取决于程序员的选择。关键是需要一个成员初始化列表

class WithReferenceAndConstMembers {
public:
	int i = 0;					//就地初始化
	int &r;					//引用成员,没有就地初始化
	const double PI;				//const成员,没有就地初始化
} ws;


class WithReferenceAndConstMembers {
public:
	int i = 0;				//就地初始化
	int &r;					//引用成员,没有就地初始化
	const double PI;				//没有就地初始化
	WithReferenceAndConstMembers() :r(i), PI(3.14) {}  //这时r和PI必须在成员初始化列表初始化

} ws;

 成员按照在类体内定义的先后次序初始化,与出现在初始化位置列表的次序无关,例如:

class A {
	int i;
	int j;
public:
	A( ):j(0),i(0) {	//还是先初始化i, 后初始化j。初始化按定义顺序
		i = 1; j = 1; 	//在函数体内不应看做初始化,而是赋值	
            } 	
    };

4.6.2 合成的默认构造函数

4.6.3 隐式的类型转换和显式构造函数

4.6.4 拷贝构造函数

 如果class A的构造函数的第一个参数是自身类型引用(const A &或A &), 且其它参数都有默认值(或没有其它参数),则此构造函数是拷贝构造函数。
class ACopyable{ public: ACopyable() = default; ACopyable(const ACopyable &o); //拷贝构造函数 };
 如果没有为类定义拷贝构造函数,编译器会为我们定义一个合成的默认拷贝构造函数,编译器提供的合成的默认拷贝构造函数原型是ACopyable(const ACopyable &o);
 用一个已经构造好对象去构造另外一个对象时会调用拷贝构造函数

class A{
public:
	A( ) = default;
};
const A o1;
A o2(o1);
//编译通过,说明A o2(o1)调用的合成的默认拷贝构造函数A(const A & )。因为:若合成的默认拷贝构造函数是A(A &),实参o1是无法传给A &形参的,因A &是无法引用const A的

class A{
public:
	A( ) = default;
	A(A &o) { cout 

什么时候会调用拷贝构造函数?
1.用一个已经存在的对象去构造另外一个对象,包括如下形式:
A o1; A o2(o1); A o3 = o1; A o4{o1}; A o5 = {o1};
2.拷贝构造函数更多的用在函数传递值参和返回值参时,包括:
i. 把对象作为实参传递给非引用形参
ii. 返回类型为非引用类型的函数返回一个对象

为什么传入const引用?

const引用可以接受const对象,临时对象,适用面更广
为什么是传引用而不是值
如果拷贝构造函数按值传递参数(即传递对象的副本),那么在调用拷贝构造函数时,C++ 需要对传递的对象参数进行复制,这会再次调用拷贝构造函数,导致递归调用,最终导致无限递归,并引发栈溢出。

(1)对象构造的编译器优化

例1:

class ACopyable{
public:
	ACopyable() = default;
	ACopyable(const ACopyable &o){//拷贝构造函数
		 cout 

c++11禁用RVO优化输出:
image
Visual studio(c++17)输出:

pass by value:
pass by reference:

RVO(Return Value Optimization) 是 C++ 编译器的一种优化技术,它优化了通过返回值构造对象的场景,减少了不必要的临时对象创建和拷贝操作。RVO 主要是在函数返回对象时进行优化,通过省略临时对象的构造和拷贝,直接将返回的对象构造在调用者期望的内存位置上,从而提高性能。
关闭方法: g++ -std=c++11 -O0 -fno-elide-constructors main main.cpp
例2:

class A{
public:
    A() { cout

理论输出

Construct
Copy Construct
Copy Construct

实际上经过优化只输出了Construct

(2)深拷贝与浅拷贝

  • 编译器提供的合成的默认拷贝构造函数其行为是:按成员依次拷贝。如果用类型A的对象o1拷贝构造对象o2,则依次将对象o1的每个非静态数据成员拷贝给对象o2的对应的非静态数据成员,其中
  1. 如果数据成员是内置类型、指针、引用,则直接拷贝
  2. 如果数据成员是类类型,执行该数据成员类型的拷贝构造函数
  3. 如果是数组(不是指针指向的),则逐元素拷贝;如果元素类型是类类型,逐元素拷贝时会调用元素类型的拷贝构造函数
  • 按成员依次拷贝也被称为浅拷贝。当函数的参数为值参(非引用)时,实参传递给值参(非引用)会调用拷贝构造函数,如果拷贝构造函数的实现为浅拷贝(如编译器提供的合成的默认拷贝构造函数),就存在如下问题:
  1. 若实参对象包含指针类型(或引用)的实例数据成员,则只复制指针值而未复制指针所指的单元内容,实参和形参两变量的指针成员指向同一块内存。
  2. 当被调函数返回,形参对象就要析构,释放其指针成员所指的存储单元。若释放的内存被操作系统分配给其他程序,返回后若实参继续访问该存储单元,就会造成当前程序非法访问其他程序页面,导致操作系统报告一般性保护错误。
  3. 若释放的内存分配给当前程序,则变量之间共享内存将产生副作用。
  • 深拷贝:在传递参数时先为形参对象的指针成员分配新的存储单元,而后将实参对象的指针成员所指向的单元内容复制到新分配的存储单元中。必须进行深拷贝才能避免出现内存保护错误或副作用
     为了在传递参数时能进行深拷贝,必须自定义参数类型为类的引用的拷贝构造函数,即定义A (A &)、A (const A&) 等形式的构造函数。建议使用A (const A&) 。
     拷贝构造函数参数必须为引用(为什么)。
点击查看代码
struct A {
     int *p;
	int size;
	A (int s):size(s), p(new int[size]){ }
	A( ):size(0),p(0){ }
	A (const  A &r) ;  	//A  (const A&r)   自定义拷贝构造函数
	~A( ) { if(p) {delete p; p=0;}}
};
A::A (const A  &r)   { 		//实现时不是浅拷贝,而是深拷贝
	p=new int[size=r.size]; 		//构造时p指向新分配内存        
	for (int i =0;  i

(3)赋值运算符重载函数

 与拷贝构造函数一样,如果类未定义自己的赋值运算符函数,编译器会提供合成的赋值运算符函数。
 编译器提供的合成的赋值运算符函数是浅拷贝赋值。因此和拷贝构造函数一样,在必要的时候需要自己定义赋值运算符函数,且实现为深拷贝赋值。

class A{
public:
//赋值运算符返回非const &,参数是const 引用
A & operator=(const A &){ …}
}
class MyString {
private:
    char *s;
public:
    MyString(const char *t = “”){ …}
    ~MyString() { … }
    MyString & operator=(const MyString &rhs); //重载=
};
MyString& MyString::operator=(const MyString &rhs) { //实现为深拷贝赋值
    char *t = static_cast(malloc(rhs.len));
    strcpy(t,rhs.s);
    //把右边对象的内容复制到新的内存后,再释放this->s,这样做最安全
    if(this->s) {   delete[] this->s; }  //这里为什么需要判断?
    this->s = t;
    return *this;  //最后必须返回*this
};
MyString s1(“s1”),s2(“s2”); s1 = s2; 
千百度
© 版权声明
THE END
喜欢就支持一下吧
点赞5 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容