【学习笔记】《C++ Primer Plus 第六版》

目录

第一章 预备知识

第二章 开始学习C++

1、名称空间

2、定义变量

3、赋值语句

第三章 处理数据

1、变量名

2、整形

3、浮点数

第四章 复合类型

1、字符串

a) 概念

b) 字符串输入

2、String类简介

a)相关函数

b)原始字符串

3、指针

4、C++数据存储

a)自动存储

b)静态存储

c)动态存储

5、内存空间

第五章 循环和关系表达式

1、延时循环

2、基于范围的for循环

3、cin读取之文件尾EOF

第六章 分支语句和逻辑控制符

1、字符函数库cctype

2、简单文件的输入输出

第七章 函数——C++的编程模块

1、函数和二维数组

2、函数指针

第八章 函数探幽

1、内联函数

2、函数参数的传递

3、默认参数

4、函数重载

5、函数模板

6、练习题

a)P299 5:

b)6:

第九章 内存模型和名称空间

1、单独编译

2、自动变量

3、静态存储持续性变量(全局变量+狭义静态变量)

a)链接性

b)静态变量的初始化

c)单定义规则(One Definition Rule,ODR)

4、练习题

a)P339第三题:

b)第四题

第十章 对象和类

 


想说的话:在学习过程中遇到我不了解的或者感兴趣的,会花大篇幅来讨论;对于很熟悉的内容在书上过一遍后,不会记录在本文中。这也算是对本科所学知识的一次系统的复习。编程环境:Visual Studio 2017

Stay hungry ,stay foolish.

工欲善其事,必先利其器。

 

第一章 预备知识

泛型编程:generic programming

 

第二章 开始学习C++

C语言的传统是头文件使用扩展名.h,C++的头文件则没有扩展名。

1、名称空间

以厂商为例,名称空间指代不同的厂商,这样将多个厂商的代码组合时不会互相混淆。

  • Micro::func(1);
  • Macro::func(1);

也可以用using编译指令,using namespace Micro,使得Micro名称空间中的所有名称可用。

2、定义变量

C++的做法是尽可能在首次使用变量前声明它。(为什么要声明变量?防止拼写错误而使用了错误的变量。。)

3、赋值语句

  • 等号表达式的值为左值,从右往左结合:b = a = 1;等价于 b = (a = 1) . 即可以连续赋值。
  • 逗号表达式的值为右值,从左往右结合:int i = 0; int n = (i += 2, i++); n结果为2 。int n = (i += 2, ++i)结果则为3。

 

第三章 处理数据

1、变量名

以两个下划线或下划线和大写字母开头的名称被保留给编译器,以一个下划线开头的名称也被保留,用作全局标识符。如_time_stop、_Dount虽然可以通过编译但不建议。

2、整形

规定整形长度为:2Byte <= short <= 4B <= int <= long <= 8B <=long long

对于有符号整数,假设长为n位(bit),则其表示范围为 [-2^(n-1),2^(n-1)-1]。指数减一是因为留一位给符号位,结果减一是因为最高位为n-2。如signed short 范围为-2^15到2^15-1(-32768~32767) 。无符号数的表示范围扩大一倍即可。

如何判断溢出?逆向运算再比较:

int tadd_ok(int x,int y){
    int sum=x+y;
    return (sum-x==y)&&(sum-y==x);
}

cout<<hex cout<<oct分别输出16进制、8进制整数。

 

3、浮点数

浮点数表示带小数的数字,一部分表示值,另一部分用于对值进行放大和缩小。

  • float的存储方式为:

float类型的存储方式

  • double的存储方式为:

double类型数据的存储方式

 

第四章 复合类型

 

1、字符串

a) 概念

在C++中,单引号表示字符常量,如 ‘S’;双引号表示字符串常量,如 “S”。

  • 字符常量:‘S’ 只是83的另一种写法,本质是ASCII码
  • 字符串常量:“S” 看起来只有一个字符S,其实编译器给后面加了个\0。实际上,“S”表示的是字符串所在的内存地址。、

b) 字符串输入

cin使用空白(空格、制表符和换行符)来确定字符串结束的位置,所以用cin输入“aaa bbb”时,第一个字符串接收aaa,第二个接收bbb,不能同时接受aaa bbb。要解决这个问题可以用getline() 和 get() 。cin.get() 经常用来接收输入流中未被接收的换行符。

int main(){
	//每次读取一行带空格的字符串
	using namespace std;
	const int size = 20;
	char name[size];
	char dessert1[size];
	char dessert2[size];
	cout << "enter name\n";
	cin.getline(name, size);//通过换行符来确定行尾,存储时用空字符替换换行符
	cout << "enter dessert1\n";
	cin.get(dessert1, size).get();//通过换行符确定行尾,保留换行符,所以要再调用get()取出换行符
	cout << "enter dessert2\n";
	cin.get(dessert2, size).get();
	cout << "end";
	return 0;
}

 

2、String类简介

a)相关函数

参见 C语言字符串函数探幽

b)原始字符串

将原始字符串(Raw)输出,不用转义符。如下:

cout<<R"(Jim "King" Tutt use "\n" instead of endl.) " << ' \n' ;

cout<<R"+* ("(Who wouldn‘ t?)" , she whispered.) +*" <<endl;

 

3、指针

 一个指针的一生:

int main() {
	/*1、声明指针。指针值均为0xcccccccc。造了三个仓库管理员,他们管理哪个仓库未知,也就是野指针*/
	int *point_c,*point_cpp,*p,val;
	/*2、定义|初始化指针。指针都有值了,但是他们指向的int变量均默认为-842150451。
	仓库管理员知道自己管理哪个仓库了,但仓库里面没东西*/
	point_c = (int *)malloc(sizeof(int *));
	point_cpp = new int;
	/*3、使用指针。指针指向的变量有一个明确的值了。仓库里面有货物了*/
	*point_c = 10;
	*point_cpp = 11;
	/*4、释放内存。
	指针指向的变量被清空,C指针指向的值为-572662307,C++指针也存在,但指向的值无法读取
	他们变成野指针了(迷途指针):告诉管理员,你不再管理任何仓库了*/
	free(point_c);
	delete(point_cpp);
	/*5、清空指针,指针值变为0
	把管理员删除掉了。人没了。*/
	point_c = NULL;
	point_cpp = NULL;
	return 0;
}
  • delete 只能释放new得来的地址,free同理。
  • 不能释放同一个内存块两次,但可以释放空指针。
  • 不释放将导致内存泄漏。

 

4、C++数据存储

a)自动存储

自动变量(局部变量)是在函数内部定义的常规变量,其作用域为包含它的代码块。执行到这个函数时,相关变量入栈,函数结束时,变量出栈,编译器自动释放这些变量。

b)静态存储

第九章将详解。要么在函数外定义它,要么用static。

c)动态存储

有一个称为堆的内存空间给程序员使用。可以在一个函数内分配内存,也可以在另一个函数里释放它。

 

5、内存空间

è¿éåå¾çæè¿°
C程序的内存空间

 

 

 

 

第五章 循环和关系表达式

 

1、延时循环

#include <ctime>
void my_delay(float secs) {
	clock_t delay = secs * CLOCKS_PER_SEC;//乘以系统时间得到秒数
	std::cout << "\a";//振铃
	clock_t start = clock();
	while (clock() - start < delay)//流逝的时间小于指定时间,就循环消耗cpu
		;
	std::cout << "\a";//时间到
}

2、基于范围的for循环

double prices[5] = {1.1, 1.2, 1.3, 1.4, 1.5};
for (double &x : prices)
    x *= 0.8;

3、cin读取之文件尾EOF

int main() {
	char ch;
	int count=0;
	cin.get(ch);
	while (cin.fail()==false)//检测到EOF后,cin把eofbit和failbit都设置为1,cin.fail()为true
	{
		cout << ch;
		count++;
		cin.get(ch);//调试发现,键盘键入字符后回车,cin就从输入流读取,读完了就需要再次键入
	}
	while (cin.get(ch)) { ; }//这样也行
	//注意在Unix使用Ctrl+D模拟EOF 而在Windows使用Ctrl+Z或回车
	return 0;
}

 

 

第六章 分支语句和逻辑控制符

 

1、字符函数库cctype

 

函数名称

返回值

isalnum()

如果参数是字母数字,即字母或者数字,函数返回true

isalpha()

如果参数是字母,函数返回true

isblank()

如果参数是水平制表符或空格,函数返回true

iscntrl()

如果参数是控制字符,函数返回true

isdigit()

如果参数是数字(0-9),函数返回true

isgraph()

如果参数是除空格之外的打印字符,函数返回true

islower()

如果参数是小写字母,函数返回true

isprint()

如果参数是打印字符(包括空格),函数返回true

ispunct()

如果参数是标点符号,函数返回true

isspace()

如果参数是标准空白字符,如空格、换行符、水平或垂直制表符,函数返回true

isupper()

如果参数是大写字母,函数返回true

isxdigit()

如果参数是十六进制数字,即0-9、a-f、A-F,函数返回true

tolower()

如果参数是大写字符,返回其小写,否则返回该参数

toupper()

如果参数是小写字符,返回其大写,否则返回该参数

2、简单文件的输入输出

一切皆文件。

#include <iostream>
#include <cstdlib>
#include <fstream>
const int SIZE = 60;
int main() {
	using namespace std;

	ifstream in_file;
	ofstream out_file;

	out_file.open(R"(C:\Users\Trafalgar\Desktop\a.txt)");//文件不存在则新建,存在则截断(丢弃原内容)
	double value = 3.2;
	int cnt = 10;
	while (cnt--)
	{
		out_file << value++<<" ";
	}
	out_file << "\nthis is a test\n";
	out_file.close();


	in_file.open(R"(C:\Users\Trafalgar\Desktop\a.txt)");
	if (!in_file.is_open()) {
		cout << "could not open file\n";
		exit(EXIT_FAILURE);
	}
	double sum = 0.0;
	in_file >> value;//读
	while (in_file.good()) {
		sum += value;
		in_file >> value;
	}
	if (in_file.eof()) {
		cout << "EOF\n";
	}
	else if (in_file.fail()) {
		cout << "由于数据不匹配无法读取\n";
	}
	else cout << "未知错误\n";
	cout << "sum=" << sum;
	in_file.close();
	return 0;
}

总结下:

  1. 引入头文件fstream;
  2. 创建ifstream对象/ofstream对象,假设是object;
  3. object.open();
  4. object >>或者<<(读写文件):就像cin一样来操作
  5. object.close()。

 

 

第七章 函数——C++的编程模块

 

 

1、函数和二维数组

int sum1(int(*arr)[4]) {//(*arr) 等价于 arr[]
	return 1;
}
int sum2(int arr[][4]) {//两种传递参数的方式
	return 1;
}
int main() {
	int data[3][4] = { {1,2,3,4} ,{5,6,7,8}, {9,10,11,12} };
	/*三行四列的二维数组 
    也可以理解为一个一维数组data[3],每个元素都是一个一维数组row[4]
    data是可以扩展的,但是row不能扩展,必须指定每行有多少元素,也就是为什么在声明时必须指定
	data + 1 即指向第一行
	*(data+1) 为第一行的一维数组{5,6,7,8}
	*( data + 1 ) + 1 指向第一行的第一个元素6
	*( * (data + i) + j) 等于data[i][j]
	*/
	int n;
	n = sum1(data);
	n = sum2(data);
	return 0;
}

 

2、函数指针

函数指针的意义在于让程序在不同的时间使用不同的函数。例如sum程序,第一次传递乘法函数,第二次传递加法函数,避免每次手动修改sum程序的代码,有泛型编程的思想。

using namespace std;
void estimate(int lines, double(*pf)(int)) {
	//pf是一个指向函数的指针,这个函数是什么样的呢?以int数据作为参数,返回double型数据
	cout << (*pf)(lines) << endl;// 记住这样来使用函数指针
}
double fun1(int num) {
	return 0.01*num;
}
double fun2(int num) {
	return 0.02*num;
}
int main() {
	estimate(10, fun1);//fun1作为参数,传递给了estimate函数
	estimate(10, fun2);
	return 0;
}

3、练习题

P252练习题10:编写使用函数指针的函数,要求使用指针数组来循环调用不同的函数。

double add(double x, double y) {
	return x + y;
}
double multiply(double x, double y) {
	return x * y;
}
double sub(double x, double y) {
	return x - y;
}
double(*pa[3])(double, double) = { add,multiply, sub };//函数指针数组,里面存了3个函数
double calculate(double x, double y, double(*pa)(double, double)) {//传递函数指针
	return (*pa)(x, y);//函数指针的正确使用
}
int main() {
	using namespace std;
	double x, y;
	do {
		cin >> x >> y;
		for (int i = 0; i < 3; i++) {
			cout<<"method"<<i+1<<": ans="<<calculate(x, y, pa[i])<<endl;//直接传递函数指针pa[i]
		}
	} while (x != -1);
	return 0;
}

 

 

第八章 函数探幽

 

1、内联函数

常规函数的执行过程:执行到函数调用指令时,程序存储该指令的内存地址,把函数参数压入堆栈中,然后跳到函数起点的内存单元,执行函数代码,最后跳回到被保存的指令处,函数参数出栈,再继续运行函数调用指令的下一条指令。来回跳跃并记录跳跃位置意味着产生了一定的开销。

而内联函数在编译时,“函数调用”语句被替换为相应代码块,程序直接顺序执行而不是跳到别处。这样就节省了程序的开销,但需要占用更多的内存(造成代码的膨胀)。【以空间换时间】

实现方法:省略原型,把整个定义(函数头和函数体)放在本应提供原型的地方。

 

2、函数参数的传递

C语言有值转递(值拷贝,新建一个副本)和指针传递(传递参数的内存地址过去)。C++有值传递和引用传递(形参成为实参的别名)。

 

3、默认参数

int add(int x, int y = 2, int z = 3) {//为y设置默认值,则y后面所有参数也要设置默认值
	return x + y + z;
}
int main() {
	std::cout << add(1) << add(1, 1) << add(1, 1, 1);
        //输出1+2+3=6 、 1+1+3=5 、1+1+1=3
	return 0;
}

 

4、函数重载

C++实现重载的机制是基于名称修饰(name decoration)(也好像是函数签名?):

1. C编译器的函数名修饰规则 

    对于__stdcall调用约定,编译器和链接器会在输出函数名前加上一个下划线前缀,函数名后面加上一个“@”符号和其參数的字节数。比如 _functionname@number。

__cdecl调用约定仅在输出函数名前加上一个下划线前缀。比如_functionname。 __fastcall调用约定在输出函数名前加上一个“@”符号。后面也是一个“@”符号和其參数的字节数,比如 @functionname@number 

2. C++编译器的函数名修饰规则 

   C++的函数名修饰规则有些复杂。可是信息更充分,通过分析修饰名不仅可以知道函数的调用方式。返回值类型,參数个数甚至參数类型。无论 __cdecl,__fastcall还是__stdcall调用方式,函数修饰都是以一个“?”開始,后面紧跟函数的名字。再后面是參数表的開始标识和 依照參数类型代号拼出的參数表。

对于__stdcall方式,參数表的開始标识是“@@YG”,对于__cdecl方式则是“@@YA”。对于 __fastcall方式则是“@@YI”。參数表的拼写代号例如以下所看到的: 
X--void    
D--char    
E--unsigned char    
F--short    
H--int    
I--unsigned int    
J--long    
K--unsigned long(DWORD) 
M--float    
N--double    
_N--bool 
U--struct 
.... 
指针的方式有些特别。用PA表示指针,用PB表示const类型的指针。

我们定义void func(int x) {} 和void func(float x) {},编译,查看汇编代码:

可以看出C++程序实际上存储了两个不同的函数。而从引用文字标红部分得知只能存储一个函数,无法重载。

另外,C++和C程序编译完成后在目标代码中名称修饰规则不同导致了一个问题:无法在C++程序中调用C的函数。解决方法是使用 extern “C”声明要引用的函数,告诉链接器在链接的时候用C函数名称规范来链接,如下:

//这里是C的头文件
extern "C" { 
    void fun1(int arg1); 
    void fun2(int arg1, int arg2); 
    void fun3(int arg1, int arg2, int arg3); 
}

 

5、函数模板

函数模板可以将同一个算法用于不同类型的参数。【泛型】

//template <class AnyType> 老式模版
template <typename AnyType>
void swap(Anytyoe &a,Anytype &b){……}
 

6、练习题

a)P299 5:

编写模板函数,求出不同类型数组的最大值。

#include <iostream>
template <typename T>
T max5(T arr[]) {
	T max = arr[0];
	for (int i = 0; i < 5; i++) {
		if (arr[i] > max) max = arr[i];
	}
	return max;
}

int main() {
	int arr1[5] = { 1,2,3,4,5 };
	double arr2[5] = { 1.0,2.0,3.0,4.0,5.1 };
	std::cout << max5(arr1) << " " << max5(arr2);
	return 0;
}

b)6:

编写模板函数,返回数组中最大的元素,并具体化。(具体化不是很懂,是模板具体到一种指定的功能吗)

template <typename T>
T maxn(T in_array[], int array_size)
{
    T max = in_array[0];
    for (int i = 0; i < array_size; i++)
    {
        if (max < in_array[i])
        {
            max = in_array[i];
        }
    }
    return max;
}
// 显示具体化
template <> const char * maxn(const char *in_str[], int n)
{
    const char * str = in_str[0];
    for (int i = 0; i < n; i++)
    {
        if (strlen(str) < strlen(in_str[i]))
        {
            str = in_str[i];
        }
    }
    return str;
}

 

第九章 内存模型和名称空间

 

 

1、单独编译

头文件里存放声明,源文件存放定义(实现)。头文件使用#ifndef---#define---//code---#endif来防止重复编译。

 

2、自动变量

先来看一个例子:

#include <iostream>
int main() {
	int a = 1;
	int b = 1;
	{
		int a = 2;
		b = 2;
		std::cout << a << b << std::endl;//输出2 2
	}
	std::cout << a << b;//输出1 2
	return 0;
}

变量a、b最开始拥有一片自己的内存。进入函数体后,系统新建了另一个同名变量a,赋值,压入栈中,更改b的值。离开函数体时,变量a出栈。举个例子:小明和小红手里都有一块钱。此时时间静止了(进入函数体),突然冒出一个也叫小明的人(入栈),他有两块钱,然后小红手里多了一块钱。这时后面的小明突然消失了(出栈),时间恢复到原来的地方(离开函数体)。小明手里还是一块钱,小红手里缺变成两块钱。

这说明,自动变量的作用域就是他们自己的代码块(函数体),但是产生同名变量时,作用域更大的那个会被屏蔽掉。

 

3、静态存储持续性变量(全局变量+狭义静态变量)

a)链接性

静态存储持续性变量有三种链接性:

  • 外部链接性:可在其他文件中访问---全局变量,在代码块的外面声明
  • 内部链接性:只能在当前文件中访问---在代码块的外面声明并使用static修饰
  • 无链接性:只能在当前函数或代码块访问---在代码块的里面声明并使用static修饰
int global1 = 0xFFFFFFFE;//外部可链接,不是可用(使用extern)
int global2;
static int one_file1 = 0xFFFFFFFD;//外部不可链接,但是该文件内所有代码可链接
static int one_file2;
void func() {
	static int count = 0xFFFFFFFC;//只有变量所处的代码块才能链接
        static int count;
}

所有静态存储持续性变量在整个程序的执行期间都存在,他们位于内存空间的数据段(data)而不是栈。

b)静态变量的初始化

程序编译后,通过监视找到变量的内存地址,再去内存看看他们的数据:

发现系统把已初始化的静态变量存放在连续内存中(data段),未初始化的静态变量放在另一片连续内存中(bss段),且赋初值0(实际上这一块内存都已初始化为0,这叫做0初始化):

 

c)单定义规则(One Definition Rule,ODR)

单定义规则指出变量只能有一次定义。C++为此提供了两种变量声明:

  • 定义声明(defining declaration):简称定义,给变量分配存储空间;
  • 引用声明(referencing declaration):简称声明,不给变量分配存储空间,因为他引用已有的变量。
//file01.cpp
int dog = 10;//定义
extern int cat = 10;//等价于int cat=10,定义
extern int mouse;//声明文件2的变量
//file02.cpp
int mouse = 1;
extern int cat;//声明文件1的变量

 

4、练习题

a)P339第三题:

使用定位new运算符

#include <iostream>
#include <new>
struct chaff
{
	char dross[16];
	int slag;
};
char buffer1[64];//静态空间的内存

int main() {
	memset(buffer1, 'b', sizeof(buffer1));//为方便观察内存,使用b填充
	chaff *p1 = new(buffer1) chaff[2];//获得静态空间的内存
	memset(&p1[0], 'c', sizeof(p1[0]));
	memset(&p1[1], 'd', sizeof(p1[1]));//两个结构体数组分别赋值
	
	char *heap = new char[64];//堆中的内存
	memset(heap, 'b', sizeof(*heap));//为方便观察内存,使用b填充 这里观察到只有第一个字节被填充
	chaff *p2 = new(heap) chaff[2];//获得堆中的内存
	memset(p2, 'c', sizeof(*p2)*2);

	char stack[64];//栈中的内存
	memset(stack, 'b', sizeof(stack));//为方便观察内存,使用b填充
	chaff *p3 = new(stack) chaff[2];//获得栈中的内存
	memset(p3, 'c', sizeof(*p3)*2);

	delete[] heap;//静态空间的内存不用管,栈中的内存出栈后自动释放,堆中的内存要手动释放
	//定位new运算符等于说从一片已申请的内存中再申请,所以无须释放
	return 0;
}

静态空间的内存分配成功(堆和栈的也是,就不截图了):

这里引出一个问题:char *heap = new char[64]; sizeof(*heap)得到的是1,怎样得到64呢?

我的想法:heap是一个指向字符数组的指针,他的地址就是字符数组的首地址。不同于char stact[64],sizeof并不能知道heap结束的地址,所以只能输出1。这还得看看sizeof具体的原理。——2019.1.8

总结:C++能在指定的内存处使用内存,比C的内存调用机制又进了一步,这也是C++的奇妙之处。

 

b)第四题

名称空间:

// main.cpp

#include <iostream>
#include "sales.h"
int main(int argc, char **argv)
{
    SALES::Sales sales1;
    SALES::Sales sales2;

    double ar[3] = { 32.1, 23.2, 65.3 };
    SALES::setSales(sales1, ar, 3);
    SALES::setSales(sales2);

    SALES::showSales(sales1);
    SALES::showSales(sales2);
    return 0;
}


// sales.h

#pragma once
#include <iostream>
namespace SALES
{
    const int QUARTERS = 4;
    struct Sales
    {
        double sales[QUARTERS];
        double average;
        double max;
        double min;
    };
    void setSales(Sales & s, const double ar[], int n);
    void setSales(Sales & s);
    void showSales(const Sales & s);
}


// sales.cpp

#include "sales.h"
namespace SALES
{
    using namespace std;
    void setSales(Sales & s, const double ar[], int n){
        ;
    }

    void setSales(Sales & s){
        ;
    }

    void showSales(const Sales & s){
        ;//为节省空间删去代码
    }
}

总结:头文件写声明,最好使用预定义防止重复编译(#pragma once);对应的源文件写实现(定义);main文件作为入口包含应有的头文件就行。命名空间有种类的感觉。

 

 

第十章 对象和类

OOP(Object Oriented Programming)的特性:

  • 抽象
  • 封装和数据隐藏
  • 多态
  • 继承
  • 代码的可重用性

采用OOP方法,首先从用户的角度考虑对象——描述对象所需的数据以及描述用户与数据交互所需的操作。前者作为私有数据成员,后者作为公有成员函数提供访问数据的唯一途径。OOP强调程序如何表示数据。

1、构造函数

类将部分数据隐藏,这就导致无法像初始化结构体一样初始化一个类。只能通过接口函数来给隐藏数据初始化。所以C++提供了一个特殊的成员函数——构造函数来实现初始化。

有两种使用构造函数的方法:

  • 显式:Stock food=Stock("World", 200,1.5);
  • 隐式:Stock food("World", 200,1.5);

类中没有定义任何构造函数时,编译器会提供一个默认的空的构造函数——里面什么都没有。因此Stock food只会创建一个类而不是给它初始化。

这里要区分两个概念(还是不太懂):

1、默认构造函数:在未提供显式初始值时,用来创建对象的构造函数。适用于 Stcok food ;

程序员有两种定义默认构造函数的方法:

  • 给所有参数提供默认值:Stock ( const string & co = "no name" , int n = 0);此时就不要再在写Stock();了,重复了
  • 通过函数重载来定义另一个构造函数:Stock () ;

注意程序员未提供默认构造函数,但提供了非默认构造函数时, Stcok food 的声明会出错,因为编译器不再提供空的默认构造函数

2、非默认构造函数:如 Stock ( const string & co , int n);

 

2、析构函数

析构函数完成对象生命期结束时的清理工作:主要是使用delete来释放构造函数中new的内存。如果构造函数没有new,析构函数实际无事可做。对于之前提到过的三种变量来说:

  • 静态变量(包括全局变量):析构函数在程序结束时被调用;
  • 自动变量:析构函数在执行完自己的代码块后被调用;
  • 自由变量:使用delete释放对象的内存时将被调用。

3、初始化与赋值

对于Stock类有三种初始化的方法:

Stock stock1("first", 12, 0.0);
Stock stock2 = Stock("second");//缺少参数则使用默认参数
Stock stock3{ "thrid",12 };//C++ 11
Stock stock3 = { "thrid",12 };// 等号可省略

最好不要创建对象的时候不赋值,后面再赋值,这样会产生临时变量(所以建议初始化时赋值):

Stock stock4;
stock4 = Stock("temp operator", 12, 0.0);//构造了一个临时对象,赋值给stock4,又把临时对象析构

4、this指针

现在要比较两个对象的某个值,返回最大的那个对象:比较obj1.val和obj2.val,如何实现这个函数呢?

const Stock & Stock::compare_val(const Stock & s) const//this指针作为参数传递进去了
{
/*解释下这三个const:
第一个表明返回的类型是Stock类的const引用,因为比较的两者都是const类型
第二个表明S的成员值不允许被修改
第三个表明*this也就是这个对象被const了,不允许修改
*/
	if (s.total_val > this->total_val) return s;//this->total_val可省略为total_val
	return *this;
}
//这样调用
Stock top = stock1.compare_val(stock2);

每个对象的方法中都有一个隐藏的this指针,是作为参数传递进去的。this指针指向调用这个方法的对象。this的值也就是对象的起始地址。

5、类中常量

private:
	const int Months = 12;//这个类还没创建,没有空间来存放常量,所以这是错的
	enum { Mouth = 12 };//方法1:这个枚举其实不是很理解
	static const int Months = 12;//方法2:静态常量

6、枚举类

传统的枚举例如:

enum size {Small,Medium,Large,XLarge};
enum emergency {Small,big};

里面的两个Small枚举量在同一作用域内,所以无法通过编译。正确做法是使用枚举类,把他们限制在各自的类中:

enum class size {Small,Medium,Large,XLarge};
enum class emergency {Small,big};
size s = size::Small;

 

 

第十一章 使用类

1、运算符重载

//.h
Time operator+(const Time &t)const;//声明
//.cpp
Time Time::operator+(const Time & t) const//注意格式
{
	Time sum(0, 0);
	sum.add_min(t.minutes);
	sum.add_min(minutes);
	sum.add_hour(t.hours);
	sum.add_hour(hours);
	return sum;
}
//main
Time t1(3,42);
Time t2(2);
Time t3;
t3.add_hour(25);
t3.add_min(2444);
t1.operator+(t2).show();
(t1+t2+t3).show();//有两种方法

2、友元

在实现乘法的运算符重载(Time operator*(double m)const;)时,可以做到A = B*2 ,但不能做到A = 2 *B,因为重载的运算符本质上是一个函数,B作为this指针,2作为double类型的参数,返回一个TIme类型值也就是A。那怎么办呢?只能通过非成员函数(类外函数)来解决。

也就是说在类外写一个方法:Time operator*(double m, const Time & t)来传入2和B这两个参数,返回结果A。这种方法就没有this指针可用,也就访问不了私有数据,即无法得到t.minutes 这个数据,这也是一个新的问题。所以前面要加上关键字friend 使其成为友元函数。

a)友元函数

重载 * 和  <<

//类中声明
friend Time operator*(double m, const Time & t);
friend std::ostream & operator<<(std::ostream &os, const Time&t);
	
//类外实现
Time operator*(double m, const Time & t){
    //这样就可用使用t.hour t.minute这些隐藏数据了
    return t * m;//这种方法实际还是调用乘法函数,也用到了t的隐藏数据
}
Time operator*(double m, const Time & t)
{
	return Time();
}
//main中使用
Time t1;
Time t2(2,33);
t1=2*t2;
cout<<t1;

重载运算符可以以成员函数或非成员函数实现,后者借助友元。

b)友元类

c)友元成员函数

 

3、练习题

P423第7题 设计复数类

//object.h
class complex
{
public:
	complex();
	complex(double r_,double v_=0);
	~complex();
	complex operator+(const complex &t);//this + t
	complex operator-(const complex &t);//this - t
	complex operator*(const complex &t);//this * t
	complex operator~()const;// ~this
	friend complex operator*(double m,  complex &t);//3 * this(t)
	friend std::istream & operator>>(std::istream &is,  complex&t);
	friend std::ostream & operator<<(std::ostream &os, const complex&t);
	bool err = false;//设置一个错误指针 输入错误时为true 这里应该还能改进下
private:
	double r;//实数部分
	double v;//虚数部分
};

//object.cpp
complex operator*(double m,  complex & t)
{
	t.r *= m;
	t.v *= m;
	return t;
}
std::istream & operator>>(std::istream & is,  complex & t){
	cout << "real: ";
	is >> t.r;
	if (!t.r ) {//调试发现 输入q时,r不会有任何变化还是0
		t.err = true;
		return is;
	}
	cout << "imaginary: ";
	is >> t.v;
	
	return is;
}
std::ostream & operator<<(std::ostream & os, const complex & t){
	os << "(" << t.r << "," << t.v << "i)";
	return os;
}
complex::complex(){
	r = 0.0;
	v = 0.0;
}
complex::complex(double r_, double v_){
	r = r_;
	v = v_;
}
complex::~complex(){
}
complex complex::operator+(const complex & t) {
	r += t.r;
	v += t.v;
	return *this;
}
complex complex::operator-(const complex & t){
	r -= t.r;
	v -= t.v;
	return *this;
}
complex complex::operator*(const complex & t){
	r *= t.r;
	v *= t.v;
	return *this;
}
complex complex::operator~()const {
	complex temp=*this;
	temp.v *= -1;
	return temp;
}

//main.cpp
int main(int argc, char **argv)
{
	complex a(3.0, 4.0);
	complex c;
	cout << "Enter a complex number (q to quit):\n";
	while (cin>>c)
	{
		cout << "c is " << c << '\n';
		cout << "complex conjugate is " << ~c << '\n';
		cout<< "a is " << a << '\n';
		cout << "a + c is " << a + c << '\n';
		cout << "a - c is " << a - c << '\n';
		cout << "a * c is " << a * c << '\n';
		cout << "2 * c is " << 2 * c << '\n';
		cout << "Enter a complex number (q to quit):\n";
		if (c.err == true) break;
	}
	cout << "Done!";
	return 0;
}

运行结果:

 

 

第十二章 类和动态内存分配

1、动态内存和类

C++让程序在运行时决定内存分配,而不是编译时(使用动态内存而不是一次性申请内存却浪费大部分)。

2、复制构造函数

以下四种方式会调用复制构造函数。(因为复制了一个副本然后赋值?)

	StringBad s;
	StringBad s1(s);
	StringBad s2 = s;
	StringBad s3 = StringBad(s);
	StringBad * p = new StringBad(s);

编译器自动声明的复制构造函数只能实现浅复制(指针复制),如果构造函数new了对象,需要重写来实现内存复制。

inline StringBad::StringBad(const StringBad & t)
{
	count++;
	str = new char[t.len + 1];
	std::strcpy(str, t.str);
}

3、赋值运算符

S1=S2;

先判断是不是自身,再删除自己的内存,再把S2复制到S1,返回S1

StringBad &StringBad::operator=(const StringBad&t) {
	if (this == &t) return *this;
	delete[] str;
	str = new char[t.len + 1];
	std::strcpy(str, t.str);
	return *this;
}

4、综合案例

a)String类

书中花了较大篇幅来描述一个String类,我开始觉得没什么意思,后来觉得有必要分析下,把声明写出来。

/*ADT:String类实现一个字符串类
数据:一个指向字符的指针,记录首地址?
字符串长度
总的字符串个数
方法:
赋值=
下标[]
比较==
输入输出<< >>
长度
*/
using std::ostream;
using std::istream;
class String
{
public:
	//构造函数和方法
	String();//默认构造函数
	String(const char * s);//通过C风格字符串构造,注意不要全设默认值,这就变成默认构造函数了
	String(const String &);//复制构造函数
	~String();//析构函数
	int length() const;//返回长度 这个const是修饰this指针的
	//用成员函数重载运算符
	String& operator=(const String&);//参数是一个String类的常引用,返回常引用
	String& operator=(const char *);//C风格字符串的赋值 strcpy
	char& operator[](int i);//return str[i]
	const char& operator[](int i)const;//这里重载不是很理解,他们的内容一样,而且本来就是const的
	//用友元函数重载运算符(这里什么时候用友元来重载还是不太清楚)
	friend bool operator<(const String &st1, const String &st2);
	friend bool operator>(const String &st1, const String &st2);
	friend bool operator==(const String &st1, const String &st2);//strcmp
	friend ostream& operator<<(ostream &os, const String &st);//把字符串st赋给输出流os
	friend istream& operator>>(istream &is,  String &st);//从输入流is读出数据,赋给字符串st
	//静态函数
	static int HowMany();//静态函数返回静态变量num_strings
private:
	char *str;
	int len;
	static int num_strings;//计数
	static const int CINLIM = 80;//静态常量,表示最大输入字符串长度
};

b)Queue类

然后是Queue队列类。

先引入一个问题:类的私有变量中有一个常量:const int qsize,表示队列最大长度,想在创建对象时候初始化。那么应该在构造函数中给它赋值:qsize=xx ; 但是这就违背了一个原则:常量在被分配内存时就应该赋值。

例如 Queue q1,我们首先申请了一片内存,私有数据都有内存了,但是没有数值。然后调用构造函数来赋值。所以问题在于,我们应该在给常量申请内存时候就赋值,而不是等到构造函数体内赋值。解决方法是成员初始化列表,把所以数据提前到申请内存时赋值。

Queue::Queue(int qs):qsize(qs),front(NULL),rear(NULL),items(0){}

int game = 10 是等价于 int game (10) 的。

C++11允许类内初始化,与成员列表初始化等价:

class book{
    int price = 10;
    String name = "NULL";
}

'

5、练习题

a)P478 1:

class Cow
{
public:
	Cow();
	Cow(const char * nm, const char * ho, double wt);
	Cow(const Cow &c);
	~Cow();
	Cow & operator=(const Cow &c);
	void ShowCow() const;

private:
	char name[20];
	char * hobby;
	double weight;
};
//实现
Cow::Cow()
{
	
}

Cow::Cow(const char * nm, const char * ho, double wt)
{
	strncpy(name, nm,20);
	int len = strlen(ho);
	hobby = new char[len + 1];
	strncpy(hobby, ho, len);
	hobby[len] = '\0';
	weight = wt;
}

Cow::Cow(const Cow & c)
{
	strncpy(name, c.name, 20);
	int len = strlen(c.hobby);
	hobby = new char[len + 1];
	strncpy(hobby, c.hobby, len);
	hobby[len] = '\0';
	weight = c.weight;
}

Cow::~Cow()
{
	delete[] hobby;
	hobby = nullptr;//清除内存后还要清空指针 不让其变成野指针
}

Cow & Cow::operator=(const Cow & c)
{
	strncpy(name, c.name, 20);
	int ho_length = strlen(c.hobby);
	hobby = new char[ho_length + 1];
	strncpy(hobby, c.hobby, ho_length);
	hobby[ho_length] = '\0';
	weight = c.weight;
	return *this;
	// TODO: 在此处插入 return 语句
}

void Cow::ShowCow() const
{
	cout << "Cow name: " << name << endl;
	cout << "Cow hobby: " << hobby << endl;
	cout << "Cow weight: " << weight << endl;
}

b) 4:手写栈

代码在这里

 

第十三章 类继承

1、派生类和基类

最开始的类称为基类(始祖巨人),继承自基类的类称为派生类(进击の巨人)。派生类需要自己的构造函数,可以根据需要添加额外的数据成员和成员函数。

派生类的构造函数有两种写法:

class man{
public:
	man();
	man(int age_, int height_);
	~man();
private:
	int age;
	int height;
};

class teacher :public man {
public:
	teacher(char subject_, int age_, int height_);
	teacher(char subject_, const man &m);
private:
	char subject;
};
//实现
teacher::teacher(char subject_, int age_, int height_) :man(age_, height_)
{//使用所有数据初始化派生类
	subject = subject_;
}

teacher::teacher(char subject_, const man & m) : man(m), subject(subject_)
{//使用基类初始化派生类
}

创建时先创建基类,再创建派生类;析构时先析构派生类,再析构基类。

派生类不能使用基类私有的数据和方法,可以使用公有的和保护的。

2、特殊关系

派生类和基类指针转换有点难理解,举例如下:

始祖巨人Origin派生出C巨人Origin_C。C巨人只能用始祖巨人的公有/保护数据和方法,这个前面说过了。但是指向C巨人的指针可以隐式转换为指向始祖巨人的指针:

Origin_C c(……);
Origin &r = c; r.name();//r可以使用始祖巨人的技能
Origin * p = &c;c->name();

但是指针转换反过来就不行,始祖巨人怎么会知道C巨人的技能是什么呢?

总结:派生类可以看做基类的一个参数。对于派生类,他可以转换成指向基类的指针,从而使用基类的公有方法(也可以直接使用);对于基类,派生类可以作为他的参数,但是不能使用派生类的任何方法。公有继承建立一种is-a(派生类 is a kind of 基类)关系,即派生类对象也是一个基类对象,能对基类进行的操作,就能对派生类进行。

 

3、使用虚函数实现多态公有继承

在派生类中重新定义基类的方法最好使用虚方法。

假设基类对象为origin,派生类对象为origin_c。他们都有一个同名方法show()和析构函数。现在有两个引用(或指针):

origin& o1 = origin ;
origin& o2 = origin_s;

这样做是合理的,前面说过。o1和o2分别是对基类的引用,但注意,o2引用的对象是派生类的类型。那么使用show时会发生什么?

如果不使用virtual修饰,那么show方法都是基类的方法,析构函数也都是基类的析构函数(对一个派生类的引用使用基类析构可不好)。反之,给基类的show()使用virtual修饰(而不是派生类),o2.show方法会更聪明地使用派生的方法(因为o2引用的对象是派生类)。

总结:派生类可以被隐式转换为基类(用途:使用数组,统一存储各种派生类和基类),但是派生类又不是基类,不能使用基类的析构函数和同名函数,所以声明时用virtual修饰,到用的时候就会智能地选择对应的方法。

提示:通常应该给基类提供一个虚析构函数,即使基类不需要析构函数(为空)。

 

4、动态联编——虚函数的实现

静态联编(早期联编)是指在编译时就确定该执行哪个函数,动态联编(晚期联编)是指在运行时确定(只针对虚方法)。

我们假设只有静态联编,对于o1和o2,编译器永远都执行origin类的同名函数,因为他不会去管o1、o2原本是对谁的引用/指向。 那么虚方法的实现就只能借助动态联编:每个对象新增一个隐藏成员(一个指向虚函数表(virtual functional table)的指针,虚函数表在运行时候生成吗?)。执行到同名函数show()时,通过该指针得到origin_c的虚函数表,找到虚函数表中对应的函数,执行。请看P504的图片。

解决了一个最重要的问题:引用/指向后,用一个指针指向的数组保留原始的状态。就算o2被转换为origin类型了,o2还有一个指针指向origin_c的虚函数表,存储origin_c类的虚函数地址,转而执行正确的虚函数。

再看看开销:使用虚函数,对每个类都得创建虚函数表这个数组(每个类所需存储空间增大)、每次调用虚函数都得去虚函数表中查地址。非虚函数就没有这些开销。

 

5、虚函数的形式

最后再来总结下如何正确使用虚函数:只在基类的同名方法前用virtual修饰(不用修饰派生类如果派生类永不派生、不用在实现中用virtual修饰)。返回类型协变:同名方法的参数必须相同,但返回类型可以不同,如下:

class man{
public:
    virtual man& show (int n);
}

class tecaher : public man{
public:
    virtual teacher& show (int n);//或返回类型为man&的
}

如果基类声明被重载了,应在派生类中重新定义:

class man{
public:
    virtual void show (int n);
    virtual void show (int n) const;
    virtual void show (int n ,int m);
}
class tecaher : public man{
public:
    virtual void show (int n);
    virtual void show (int n) const;
    virtual void show (int n ,int m);
}
//实现
void teacher::show (int n){
    man::show() ;
    //code
}

6、抽象基类(Abstract Base Class——ABC )

ABC至少要使用一个纯虚函数。ABC要求具体派生类覆盖其纯虚函数——迫使派生类遵循ABC设置的接口规则。

学到这里的时候,惊闻C++20要来了,而我还在学C++99/03……赶紧学吧。RunNoob~

 

7、练习题

做第一题时,遇到一个Bug,记录下:

这里pcd指针指向的对象,它的虚函数表为空,导致无法指向对应的虚函数?为什么呢,我往前看,发现c1对象是有虚函数表的。但是c2初始化后就没有了,它的performance字段还被改了:

看看c2初始化干了什么:

我觉得是没问题的,然后看c1对象的内存,调试过程中发现c2初始化后c1有部分数据被改了,另外好像第一个字符串参数只存储了很少量的字符,然后回去看派生类的私有变量:

我只预先分配了10个字节内存。。。更正后解决。

总结:因为写类变量时以为最多10个字符,没想到有30多个,导致存储空间不够,strcpy数组越界,把虚函数表的内存给覆盖了。这种问题感觉很难检查,花了很多时间,还是粗心,所以要用动态数组(第二题)。这个问题根源还是strcpy这个函数,它是不安全的。

做第二题的时候也有个Bug:派生类没有重写复制构造函数和赋值函数,导致派生类析构两次,报错。加上复制构造函数还是出差,再加上赋值函数OK。记住:赋值函数、复制构造函数是不能被继承的,一定要自己写。

今天也是被C++完虐的一天呢。

第16章 string类和标准模板库

string类

string类的构造函数

#include <string>
int main(){
    string one("hello world");
    string two(20,'$');
    string three(one);
    string four = two + three;
    string five(four,3,10);
}

string类的输入

#include <string>
#include <iostream>
using namespace std;
int main(){
	string a;
	//以字符-分隔开的字符串,这个分隔字符可以是空格
	getline(cin,a,'-');
	while (cin)
	{
		cout<<a<<endl;
		getline(cin,a,'-');
	}
	cin.get();
	return 0;
}

 

string类的使用

#include <iostream>
#include <string>
using namespace std;
int main(){
	string a("hello world");
	if(a.find("o w")!=string::npos){
		cout<<"find\n";
	}else{
		cout<<"not find\n";
	}
	cin.get();
	return 0;
}

 


知识共享许可协议

本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可

  • 11
    点赞
  • 52
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: "C Primer Plus" 是一本用于学习 C 语言的教程书。它提供了 C 语言的基础知识,包括语言语法、数据类型、控制结构、函数、指针、结构体、输入输出等内容。此外,还涵盖了一些高级主题,如内存管理、文件处理、网络编程等。"C Primer Plus" 的 PDF 版本是电子书的形式,可以在计算机或手机上阅读。 ### 回答2: "C Primer Plus" 是一本经典的C语言教程,非常适合初学者学习和理解C语言编程。它由Stephen Prata撰写,并且已经成为许多大学和学院的教材。这本教程以简洁明了的方式讲解了C语言的基础知识和常见的编程技巧。 "C Primer Plus"以PDF格式提供,这意味着读者可以通过电子阅读器或计算机来访问和学习。PDF格式的优势是可在不同设备上方便阅读,而且保留了原版书籍的布局和格式。 这本教材被广泛推荐和使用,原因在于它适用于零基础和有一定编程经验的学生。它以简单易懂的语言解释了C语言的概念,并以例子和练习来帮助读者巩固所学知识。此外,书中还包含了实际的编程项目,让学生将所学应用到实际问题中,提高他们在C语言编程方面的技能。 "C Primer Plus"覆盖了C语言的基本语法、常见的编程结构、指针、内存管理、文件处理等内容。它的目标是培养读者的编程思维和解决问题的能力,为他们打下坚实的C语言编程基础。 总而言之,"C Primer Plus"是一本非常有价值的C语言教程,提供了丰富的内容和实践项目,非常适合初学者学习和掌握C语言编程。它的PDF格式方便读者使用和学习,并且广泛推荐和使用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值