学习目标

争取熟练掌握C++,达到能够在查看标准库手册、不引入第三方库的情况下独立完成某一个项目的能力,能有python的60%功力就已经足够了,为后续的研究、项目打下基础就好。

学习体会

  • 项目文件夹命名风格:一个文件夹放一个项目,里面有一个include文件夹放头文件,一个src文件夹放源文件,main.cpp文件也需要放在这个文件夹里面。一个项目文件夹内配置一个CMakeLists.txt文件以供Cmake配置。如项目内包括了多个子项目,则另起一个文件夹,将所有子项目放在该文件夹内,并配置一个顶层的CMakeLists.txt文件。
  • 在C语言的变量初始化的基础上,C++增加了更丰富的初始化方法,可以使用默认初始化(按如下语句声明某个类的变量:classname CN),或者使用自定义初始化方法,初始化可以引入参数(类似于python的init函数),初始化方法在类的声明中直接定义,感觉这种语法,更加面向对象,接近于python的风格了。
  • lambda函数与python一样,没有必要在迭代器的外部单独定义,只需要在使用的时候定义好就行,放在头文件中不合理(因为它是一个对象,头文件中最好不要有对象存在),放在源文件中不合理(除非只在源文件中访问,否则在项目其他文件中根本使用不到)。
  • cmake完成的只是文件配置和链接,相当于生成了一个超级长的gcc命令行,除非新增加源文件和头文件改变链接库和可执行文件,否则,cmake执行过一次后就可以不用再动了,只需要反复make改变编译结果就可以了。
  • cmake里面使用include_directories命令可以在兼顾vscode联想功能的情况下同时保证可编译,推荐使用它而不是target_include_directories命令。

习题练习

4.12 复习题

a. char actor[30];
b. short betsie[100];
c. float chuck[13];
d. long double dipsea[64];
2. #include <array>
a. array<char, 30> actor;
b. array<short, 100> betsie;
c. array<float, 13> chuck;
d. array<long double, 64> dipsea;
3. int arr[5] {1,3,5,7,9};
4. int even = arr[0] + arr[4];
5. std::cout<<ideas[1];
6. char arr[] = "cheeseburger";
7. string arr= "Waldorf Salad";
8. struct fish{string name; int wight; double length;};
9. fish fi{"glodfish";10;10;};
10. enum Response{No, Yes, Maybe};
11. double *p_ted = &ted; std::cout<<*p_ted;
12. float *p_treacle = treacle; std::cout<<*p_treacle; std::cout<<*(p_treacle+10);
13. int n; std::cin>>n; int *p_arr = new int [n]; 或者int n; std::cin>>n; vector<int, n> v_arr;
14. 无效,因为该字符串并没有分配地址,不能强制类型转换为int型指针。
15. fish *p_fi = new fish; std::cout<<p_fi->name<<p_fi->wight<<p_fi->length;
16. 有可能address数组内存不够大,导致缓冲区溢出。

5.

  1. 入口条件循环是在进入循环时先判断条件再执行循环,而出口条件循环则是恰好相反。其中for循环和while循环是入口条件循环,而do while循环是出口条件循环;
  2. 打印01234,最后加入一个换行符
  3. 打印0369换行12换行
  4. 打印7换行9换行
  5. 打印 k = 8换行
  6. 使用花括号括起来的代码块
  7. 有效,它将声明一个int型变量x,并将其初始化为24;另一条语句将声明一个int型变量y,将其初始化为1
  8. cin>>ch将忽略全部空格,直到遇到换行符为止;cin.get(ch)和ch=cin.get( )都将全部字符存放至ch中直到换行符为止;

6.10

  1. 版本1中需要进行了两次if语句,而在版本2中,如果字符中空格,可以省略一次判断。
  2. 会发生一直输出第一个字符偏移1位的死循环
  3. Seendd $ct1 = 5, ct2 = 5
  4. 不同,因为第一次!将x强制类型转换为bool型变量,第二次对这个bool型变量取反,得到的只能是true或者false。

8.7

  1. 程序简短,执行时间与调用时间相差无几的函数,一般要求程序代码写在同一行里面且不能超过10行。
  2. v1是float类型,v2是float &类型,v3是float类型,v4是int类型,v5是float类型。

9.5

  1. a.使用自动变量,无链接性;b.使用静态变量,外部链接性;c.使用静态变量,内部链接性;d.使用静态变量,无链接性。
  2. using声明是将某命名空间内某几条指令名称加入至当前源文件中,如果当前文件中定义有相同名称的指令标识符,则默认使用声明的指令覆盖之;using编译是将该命名空间内的全部指令名称加入至文件中,可能会引入一些未设想的相同名称标识符指令覆盖当前指令。
  3. 显示 10换行4换行未知换行10, 1换行10, 未知换行
  4. 显示 1换行4, 1, 2换行2换行2换行4, 1, 2换行2

10.9

  1. 类是实现将抽象封装转化为用户定义的数据类型的C++工具,它将数据表现和操作数据的方法组合成为一个整体。
  2. 类实现抽象:类中提供了数据和方法的声明,将一个用户需要定义的对象抽象为数据及其处理方法;类实现封装:使用了类作用域保证类方法和类变量只在类作用域内有效类将实现细节和抽象分开,并各自存放在一起,使之成为一种C++编程规则;类的数据隐藏:类默认私有访问其变量和方法,而通过显式提供公开访问方法,向用户开放数据访问接口。
  3. 对象和类的关系:对象是一种类的实例化结果,类是一个对象的抽象类型。
  4. 类函数成员是整个相同类的全部对象共用的,这些类函数都是同一片内存空间。这些对象访问类函数的过程称为发送消息,而类数据是一个对象私有的,即使是从同一个类实例化得到的对象,也具有不同的数据存储地址。
  5. 类构造函数是对象创建过程中,被初始化时调用的。析构函数则是在对象过期后,例如自动变量在函数执行完毕,内部链接的变量在代码块执行完毕或者动态变量在使用delete释放内存时调用的。
  6. 默认构造函数是指,声明一个对象后不使用任何初始化方法,则程序将自动调用的构造函数。拥有默认构造函数后,程序将支持定义变量而不需要立即初始化。

11.8

  1. 友元函数定义在类作用域以外,不属于类,但是能够访问类的成员变量;而成员函数定义在类作用域内,只有先声明一个类型的变量后才能访问,能够直接访问类的成员变量;
  2. 非成员函数必须是友元才能访问类成员;
  3. 不能重载的C++运算符有:::, *(指针),sizeof,类型转换运算符,.
  4. 重载运算符=时,必须定义为赋值运算符;重载圆括号和方括号的限制?重载->的限制?

12.9

  1. a. 默认构造函数没有分配内存,对象内部的指针将为空;b. 将C风格字符串的首地址复制给了对象内的字符指针,没有将对象字符之前可能存在的内存释放,致使内存泄露;c. 字符指针没有分配内存,直接进行内存的复制操作将导致错误。
  2. 如果您定义了一个类,其指针成员是使用new初始化的,请指出可能出现的3个问题以及如何纠正这些问题?
  3. 将提供默认构造函数,默认复制构造函数,地址传递函数,析构函数
  4. 改正前的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class nifty
{
// data
char personality[];
int talents;
// methods
nifty();
nifty(char * s);
ostream & operator<<(ostream & os, nifty & n);
}

nifty:nifty()
{
personality = NULL;
talents = 0;
}

nifty:nifty(char * s)
{
personality = new char [strlen(s)];
personality = s;
talents = 0;
}

ostream & nifty:operator<<(ostream & os, nifty & n)
{
os << n;
}

改正后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class nifty
{
// data
char * personality; // 仅在函数原型中,指针和空数组可以替换
int talents;
// methods
nifty();
nifty(char * s);
std::ostream & operator<<(std::ostream & os, nifty & n);// 增加std作用域解析运算符
}

nifty::nifty()// 作用域解析运算符
{
personality = nullstr;// 推荐使用nullstr而不是0或者(void *) 0
talents = 0;
}

nifty::nifty(char * s)
{
delete personality;// 释放当前指向的内存
personality = new char [strlen(s)];
personality = s;
talents = 0;
}

ostream & nifty::operator<<(ostream & os, nifty & n)
{
os << personality << talents;// 具体化输出流
}
  1. a. 1、默认构造函数;2、带默认参数的构造函数;3、带默认参数的构造函数;4、默认构造函数;5、不带默认参数的构造函数;6. 带默认参数的构造函数;7. 不带默认参数的构造函数;8. 带默认参数的构造函数 b. 需要有析构函数、复制构造函数

13.10

  1. 派生类从基类那里继承到了公开类型的成员函数和变量,保护变量。
  2. 但是不能直接访问私有变量和私有成员函数。
  3. 如果返回类型为void,将导致赋值运算符返回类型为void,类baseDMA将不能正常进行赋值运算;如果类型为baseDMA而不是引用,则在赋值运算时还会进行一次复制初始化,增加了程序执行的时间。
  4. 创建派生类对象时,首先调用基类的构造函数,然后调用派生类的构造函数;删除派生类对象时,首先调用派生类的析构函数,然后再调用基类的析构函数。
  5. 仍然需要,因为派生类的默认构造函数将不包括指针地址值的复制,如果数据类型中含有动态存储,则初始化派生类将产生错误。
  6. 派生类将调用派生类的方法;
  7. 当派生类中含有动态内存变量时,需要为派生类定义赋值运算符。
  8. 可以将派生类对象的地址赋值给基类对象
  9. 但是反之不然,因为基类中并不含有派生类中的成员函数和数据变量。
  10. 可以的,因为派生类对象包含了全部的基类成员;
  11. 不可以,因为不能将派生类对象强制类型转换为基类对象;
  12. 按引用传递对象时,只需要复制对象的地址指针而不需要复制对象本体数据;按值传递对象时,需要调用复制构造函数和赋值函数。
  13. a. 将被解释为常规非虚方法;b. 将被解释为PublicCorpration::head()方法;
    1. 派生类的默认初始化函数没有初始化kit_sq_ft成员变量,此外,该派生类中重定义的area()成员方法也没有与基类原型保持一致;

14.6

  1. 1)采用公有派生;2)采用私有派生;3)采用公有派生;4)采用私有派生;5)采用私有派生;
  2. 虚基类和非虚基类的区别:虚基类用于多重继承,保证子类从不同的派生类中继承基类唯一,而排除重复部分的代码,非虚基类在多重继承中则不具有这种性质,每有一次派生类的继承,子类中便增加一处基类的代码,且可能存在因为冗余代码导致的二义性。

基本的语法学得差不多了,文件系统和标准库还没怎么细看,真要说的话C++最重要的特性多线程也还没有开始学。下一步计划是尝试用一两个小的项目把之前学过的东西熟悉一下。边学边复习。