在本章我想要以一个简单的例子来引入C++的使用,并给读者一个初印象。
我会用尽可能简单的代码来展示C++的基本功能,比如输出字符,然后让它做一些简单的计算器可以做的事情。
我还要给读者介绍一些必知必会的基本概念,本章可以算作是泛讲篇的泛讲。
这里介绍编译器的选择和语言标准的问题(本书默认使用C++17标准)。
然后带领读者编译并运行出一个最基本的C++程序(Hello World)。
这本书不负责教读者如何配置编译器。如果实在配不出来,建议读者用Coliru或者Wandbox等在线编译,效果差不多。
这里我想给读者介绍什么是数据,数据如何体现信息。
然后简介一下常用的数据类型,我在这里将其粗糙地分为整数、浮点数、字符数三类。(指针什么的,本章不讲)。
要提及,字符正是用数据来体现信息的一种形式(ASCII)。
在这里我会介绍数据如何定义(限于前面只介绍了三种类型,我在这里只用int, double和char)。
然后通过修改cout输出的内容,或者是用算术运算符来拼接它们,让它们做像计算器一样的事情。
这里我只对运算符做简单概括,不会搞得太复杂。
在这里我必须让读者建立起对类型的敏感性。
用8/3这样的经典例子来说明类型不同会让结果有很大差异。
也通过%运算符和()的使用,让读者认识到C++(乃至其它语言)对运算符的含义有各自的规定。
通过sizeof运算,让读者对“内存空间”有初步的印象,知道不同类型的内存空间不一样。
另外我还要简单提一句,sizeof和之前接触到的运算不同,它是在编译时计算出来的。
介绍下类与对象的基本概念,因为之后会频繁用到,不可不提。但是仅限于概念。
介绍如何定义和使用const常量和enum常量(enum只是开个头,以后还会用它的)。
介绍下#define(编译时操作)和它的缺陷。
(注意:常量不是一个“类型”,而是类型限定符)
int, long long, unsigned之类。介绍signed和unsigned类型的数据范围区别。
通过sizeof运算求它们的内存占用。同时指出这样的结果因系统而异,让读者对系统差异有初步印象。
还有不同进制下的数据,也可以稍微谈谈。
float, double, long double之类。介绍它们的数据范围。
介绍一下f和l后缀,并比较整型和浮点型的后缀使用情况。
char, char8_t, char16_t和Unicode的简单介绍。
夹带一点字符串,但不多讲。
bool型。特别之处在于和bool紧密相关的两个关键字ture和false。我会阐述它们和1与0的关系。
通过函数的视角来看待运算符,即每个运算符都有参数和返回值。
这里以算术运算符和赋值运算符和cout使用的左移位运算符为例,讲解其优先级的问题。
都讲到
cout<<了,顺便提一句重载吧。
这里以连续赋值a=b=c和连续除法a/b/c为例,介绍运算符的结合性。
以我所知,很多人初学的时候是并没有搞懂结合性为何物的。
以a<b<c和a==b==c为例,谈谈运算符及其可能带来的语义困惑,帮读者规避这个易踩的雷。
顺便提一句语法和语义的区别。
主要介绍整型与浮点型的差异,包括数据范围和精度。
还要介绍整型与整型、浮点型与浮点型的差异。
这样一来我就说清了“为什么要做类型转换”。
先介绍一下运算符对数据类型的隐式转换,比如char+int或者是int*double。
以1.0*a/b为例,介绍我如何使用隐式转换来避免计算整数除法时的精度损失。
还要运用结合性的知识,解释1.0*a/b和a/b*1.0的区别。
介绍C++的显示类型转换方法,包括C风格、函数风格和static_cast风格的。
我还会阐述static_cast的优点,并建议使用此方法。
至于const_cast, dynamic_cast和reinterpret_cast,我会留到精讲篇来讲。
本章主要讲解C++的流程控制语句if, for, while, switch这些。
简单介绍一下什么是编译时行为(与语法挂钩),什么是运行时行为(与语义挂钩)。
比如说sizeof和#define就是编译时行为;而类型转换和变量的计算就是运行时行为。
或许可以带上
constexpr?
可以谈谈终端如何处理输入和输出,我们键盘上的输入是如何被程序获取,内容又是如何呈现到显示屏上的。简单讲讲就行。
介绍程序的三种基本结构:顺序、选择、循环。
以&&、||、++、--运算符为例,介绍运算次序。
要简单提一句值计算和副作用的关系。
让读者从计算机的视角来理解选择结构的流程。
if-else块;单独使用if;以及else-if连用(注意,没有else if这个关键字,它只是else和if的嵌套使用而已)。
switch-case-break的使用。特别要让读者注意的是break的作用,我把它解释为:switch块当中的内容依然是顺序运行的,break起到退出的作用。
合并case也要讲一下。
这里只讲基本for循环,范围for留到讲数组。
重点在于解释for的三段语句各自的含义。
这里讲while循环。不得不提的是while(cin>>a)这样的用法。cin>>a有一个返回值,这个返回值可以隐式类型转换为bool,于是可以被while接收。
一笔带过一下do-while循环。这个东西不很常用,但是作为很基本的语法,应该知道。
介绍continue和break语句在循环体内的使用。
通过几个实验,让读者意识到有些数据是有作用域的。
更进一步,要让读者明白数据是有生存期的。
关于为什么要安排一个先讲函数然后把复合类型和函数穿插在一起讲的顺序
其实我觉得如果先讲复合数据类型再讲函数的话,数组还好说,指针真的很难讲,因为你用一些输出地址值的示例代码一点也不直观,看了半天读者可能还是不知道自己到底为什么要用指针,所以我觉得把指针放在函数参数里讲会更容易接受。
但是另一个问题是,如果我先讲函数的话,很多参数类型我没讲,所以我好像讲不了太多,单开一章显得很没必要。
我一度想要把函数、指针和数组放在同一章讲,但觉得还是不合适,因为它们虽然有交集,但还是相对有独立性的知识,揉在一起讲显得太臃肿。
所以我安排了这么一个先讲函数初步,再讲复合类型(和函数参数揉在一起)、再讲自定义类型(和返回类型揉在一起),最后回来讲函数进阶的顺序,有点无奈之举。
我们不需要知道它的构造原理,只需要知道它的用途就可以使用它。
函数是相对孤立的,只有接收信息(参数)和返回信息(返回值)的接口。
可以不必为每个功能重复写多次代码。
对函数内部作微调,一般不需要对外部进行修改。
返回类型和参数类型、函数命名和函数体。函数体中返回的值必须能转换成规定的返回类型,否则就是未定义行为。
顺便给读者普及下什么是未定义行为。
介绍下什么是实参、什么是形参,它们的区别和联系是什么。
以max为例,测试下函数能否使用。
再用多个函数组成一个例子,看一下多个函数嵌套调用的效果。
在使用函数之前就要声明,但可以把定义放在后面。
声明可以多次,定义必须唯一。
谈谈函数的副作用,以及我们如何利用副作用。
有些时候调用函数不是为了使用它的返回值(我们可能将返回类型设置为void,或者干脆用弃值表达式),恰恰是为了使用它的副作用。
介绍这种方法的同时,还要介绍下在与函数重载同时存在时,可能引发的二义性问题。
(简单水水)
以求阶乘为例,讲讲函数递归是怎么一回事。
谈谈为什么要用函数模板。不往深里讲。
用函数模板写一个max函数,看看效果。
介绍下有关内存和地址的基本概念。
如何定义指针、赋值和运算。内容访问应当用*实现。
为什么传递一个变量不能满足我们的要求?为什么传递指针能实现我们的目的?当我传入指针时,我提供了什么信息?
指针在加法运算时的返回值就是指针类型,而减法得到的是std::ptrdiff_t类型。
考虑了一下决定还是把函数指针放精讲篇。
简单讲讲它们的定义语法就行。
不过要强调下,怎么理解它们的定义语法。这对后面指针和数组的关系来说很重要。
泛讲篇不讲右值引用。
这里介绍下(左值)引用的相关知识。包括type&和const type&。
它们的地址相同,说明只是别名。但const type&不能修改变量的值。
引用传参的优点是不用特意去取参数的地址,比如cout<<var可以直接将var传入。
另外,因为不需要为另建副本,也省了内存和时间。
一维数组的定义/初始化语法比较多,都介绍下。
使用要用到下标运算符。
一维数组传参的形式。
在这里可以讲讲范围for循环了。
用typeid和is_same来看看数组的类型都是什么样的,它和指针又是什么关系。
字符串是一系列的char字符构成的一串字符,'\0是结束符。
我们可以用char数组来存储字符串,但是数组的大小和字符串的长度并不是同一个概念。
方式同样很多。
写两个简单的函数,一个是strlen,另一个是strcpy,来实现一些基本的函字符串操作功能。
字符串的输入有很多方法,也有很多注意事项,这里简单介绍cin>>、cin.get()和cin.getline()即可。多余的留给精讲篇。
另外值得强调的是输入流的问题,键盘行为和输入的字符有什么关系,cin如何解析这些字符。
相当于批量定义数组。
如何理解语法?
相当于批量定义指针。
如何理解语法(尤其是和数组指针共同出现时,极易混淆)?
指针可以指向普通数据,当然也能指向数组!
如何理解语法?
指针一经定义,就需要存储值,当然也就有了地址。
指针能指向数组,当然也能指向指针!
我们可以把数组当做指针来用,当然也可以把指针当作数组来用!
对于一阶指针,如何使用动态内存分配来分配一段内存空间,并在用完时回收。
对于二阶指针,如何使用动态内存分配来分配一段内存空间,并通过稳妥的方法来回收,从而避免内存泄漏。
布置分配(placement new)放在精讲篇。
枚举常量的好处是,可以使用有意义的单词来表达确定的含义,避免用数值这种含混不清的东西。
同时,枚举常量对本类型的可能取值作出了规定,不在规定中的名字和来自其它类型的名字都是禁止的。
scoped enumeration 放在精讲篇。这里只讲unscoped enumeration。
(简单水水)
这是很有意义的,因为在此之前我们的函数只能返回单个值,现在我们可以用结构体将多个值封装起来一起返回了。
通过对象指针成员和动态内存分配,写一个简单的单向链表。
如何利用union套struct/array的形式,让数组元素有一个有意义的名字。
关于成员变量的部分,读者已有基础,只需讲一下成员访问权限的问题。
还要稍微讲点成员函数的知识。
使用vector的成员函数,完成一些简单的功能。
介绍下标准库头文件与自定义头文件的问题。
尝试将类和函数声明在头文件,编译在definition.cpp文件,并用main.cpp调用函数和定义对象。
如何定义命名空间,怎样使用命名空间。
using namespace std的优点与缺点。
解释作用域的概念。
既然谈到了作用域,就不得不谈谈名称查找了。
简单介绍下数据的生存期:自动生存期、静态生存期、线程生存期及动态生存期。
分为外部链接、内部链接和无链接。
介绍一些常见的编码风格,并提出一些编码风格上的注意事项。
到这一章结束,读者应当具备了搭建简单工程的知识量。接下来的章节就要上强度了,我会带着读者多写一些代码的。
重载一些语法糖,让vector的操作更简单。边写边讲。
- 重载
<<运算符用于push_back。 - 重载后缀
--运算符用于pop_back,重载前缀--运算符用于pop_front。 - 重载单目
*运算符用于返回size。略微涉及一点auto和decltype的使用。 - 重载
<运算符,用于字典序比较。 - 利用已经重载好的
<运算符,重载>,==,!=,<=,>=运算符。这也是代码复用的一种方式。
(从cppreference抄吧,要不)
先讲运算符重载再讲友元也有我的考虑。这样能破除两种常见的偏见:一是“友元函数是成员函数”;二是“运算符必须定义为友元”。
以valarray类为例,定义一些函数和运算符。
基于普通数组实现,属于低配版。
重载如下运算符,边写边讲。
- 重载
=和+=。 - 重载
[],强调它不能是友元,必须是成员函数。 - 重载
+和*,既有成员函数版本,又有非成员函数版本。 - 重载
<<(ostream&,const valarray&),它是valarray的友元,但不是ostream的友元。
构造函数与拷贝构造函数(提一下深浅拷贝的问题)。
移动构造函数放精讲篇。
怎么使用初值列(成员初始化列表),为什么初值列更高效。
怎么定义和使用成员默认值,
为何使用析构函数,怎样用。
不用数组而是用指针+动态内存分配的方式来写valarray。
添加一些好用的构造函数和析构。
特别需要注意的是,=运算符也需要添加一个“自我赋值”的判断,否则将会出现问题。
包括常成员函数
类型转换可以通过转换构造函数或自定义转换函数实现,看怎么方便怎么用。
隐式类型转换可能不尽如人意,所以有些时候我们需要用显式类型转换。
介绍下用explicit来强制显式类型转换,避免不合适的隐式类型转换带来的问题。
以std::string为例,介绍下对象数组,强调不同的下标运算符的含义可能是不同的(strs[0][0]这样)。
多用
typeid,很好用的。
再尝试一下用valarray指针来分配一批对象,理顺valarray指针申请的动态内存空间和valarray构造函数申请的动态内存空间有什么区别。
简单介绍并带着读者用一下std::string,起码知道我们需要实现哪些功能。
写一写string类的定义部分,成员函数先声明出来。
把成员函数的功能写出来。
看一下我写出来的string类效果如何。
重点在于梳理清楚三组概念,不讲具体的技术。
对应C++中的对象和成员的关系,比如一个人有心脏和大脑。
对应C++中的类和对象的关系,比如人类有张三和李四。
对应C++中的基类和派生类(公有或受保护继承)的关系,比如食肉目有猫科、犬科和熊科。
讲一下公开继承(public)的语法。
谈谈公开继承的对象能访问基类的哪些信息,又如何访问。
顺便讲一下protected成员。
重点介绍派生类对象的基类成员是如何构造和初始化的。
(前述的valarri::Arr正是一个绝佳的例子)
这里通过私有继承vector<int>的方式写一个简单的stack,实现一些最基本的功能。
比较一下私有继承方式和用作成员变量各自的优劣。
受保护继承和一些细枝末节的东西都塞到精讲篇。
用一些小例子,介绍下多级继承即可。
向上类型转换和向下类型转换如何实现,为什么 static_cast 在向下类型转换时不好用了。
基类指针可以指向派生类对象;基类引用可以引向派生类对象。它们能操作哪些成员。
只剩下一种可能性:为了使用同名而不同功能的成员函数。
dynamic_cast 需要依赖多态的基类,我们要靠虚函数来实现。
定义一个抽象基类,让矩形与正方形都继承它。
std::iostream就是多重继承的绝佳实例。拿它讲就好。
在使用棱形继承时,基类的成员重复。
吐槽一下,用“虚基类”这个名字是很容易引起误解的,这个
virtual不是基类自己拥有的属性,而是“继承关系”的属性。精讲篇会细究这个问题。
我们可以忽略它的具体类型,用一套普适的方法来处理各种类型的数据。
template允许接收的参数可以是类型信息或数据信息。
注意:模板参数必须都是在编译时确定的。
显式实例化和隐式实例化。
以swap函数为例,我们可以用它来交换两个T[]数组的内容。这是一种重载。
特化模板函数,用于特殊的类型。
我们可以忽略它的部分成员的具体类型,为具有相同特征的成员搭建一个通用的类模板。
一个非常简化的array类模板,只能实现最基础、最简单的功能,但足够用来讲解了。
显式实例化和隐式实例化。
与函数模板相似,类模板也不是一个预先给定的类。编译器根据需要,会根据类模板生成若干个对应的类定义。
类模板对函数的友元、对函数模板的友元、对类的友元、对类模板的友元。
如何通过完全特化,对某些特殊的类进行不同的处理。
is_same 类模板等。
带读者试试map。
简单介绍下迭代器。
带读者试试copy, sort, unique三种算法。
更多STL知识及相关概念放精讲篇了,要不然泛讲篇就变成精讲篇了。
带读者写一个auto_ptr类模板。
虽然
auto_ptr在C++17中已经被移除了,但是我们泛讲篇不需要考虑得太细,能写出来一个简单的auto_ptr对初学者来说就已经是不小的成就了。
本章只做简单介绍,详细的放精讲篇。
写一个简单的代码来测试一下这个结构。
简单介绍下异常类及其派生类。
为array类模板设计一个成员函数at,带范围检测,继承自std::exception。
简单介绍下noexcept限定符的使用。
讲一讲设备之间如何通过流进行交互。
格式标志(format flags)和std::ios_base::hex之类的。
还有iomanip库。
good, eof等。
简单展示一些例子就行。
只讲简单的in, out, trunc和app,至于binary,放精讲篇。
分为左值和右值。
右值中划分出临终值和纯右值,而临终值与左值共同构成广义左值。
整型、浮点型等。
它们的二进制表示,以及整数的位运算。
指针、数组、引用、结构体等。
讲讲右值引用,以便后续使用。
const常量不是任何类型,它可以用于组合各种类型,以及组合成员函数(相当于组合了*this)。
volatile不讲
定义的语法和初始化的语法。
各种常见类型的初始化方法,比如直接初始化、列表初始化、统一初始化等等。
谈谈直接初始化的问题,它可能被误认为是函数定义而非初始化。
然而统一初始化也有它的问题,半斤八两吧。
不同类型的转换问题。
static_cast、const_cast、dynamic_cast和reinterpret_cast。
对象通过运算符(或直接)构成表达式。表达式可以拼接。
分号意味着一个表达式语句的结束。
还有很多其它的语句类型,比如for循环这种。
一个表达式可以有值计算和副作用,或兼而有之。
弃值表达式只用其副作用。
给函数添加[[nodiscard]]说明符可以对弃值表达式给出warning警告。
顺序点规则(简介即可)。
“按顺序早于”规则。这里不需要都讲,提几个常见的要点即可。
有些运算是未定义行为,可能会诱发未知结果,应当注意。
鉴于泛讲篇已经讲了不少,我主要对泛讲篇中没讲的部分做一下查漏补缺。
单目运算符的参数只有一个,作为成员函数时是*this本身,作为非成员函数时就需要指定。
双目运算符的参数有两个,作为成员函数时要提供另一个参数,作为非成员函数时就需要提供两个参数。
C++17不允许[]接收多个参数,所以想要访问高维数据(如矩阵)可以用()的重载来实现。
它们都必须是成员函数。
Placement new。主要是介绍下布置分配的语法和注意事项(结束一个对象的生存期时,需要主动调用析构函数)。
简要介绍下new和delete的重载。
编译器将如何根据代码中给定实参的类型,推导出函数的类型。
如何重载模板参数,来实现我们的特定目的。
显式特化与直接重载都能实现我们的目的。
定义和使用方法。
就像我们更倾向用局部变量,而不是全局变量,来实现临时功能。
我们也更倾向用局部函数,甚至是不具名函数,来实现临时功能。
lambda的定义和使用方式。
介绍一点常用的捕获方式。
什么是函数对象,为什么提出这个概念?
如何通过重载()的方式,定度函数对象?
通过几个算法,尝试使用函数对象。
public, protected, private
class默认为private,而struct默认为public。
public, protected, private
列张表概括一下,不同继承方式和访问权限组合起来是什么样的。
类中的友元函数和友元函数模板;
类中的友元类和友元类模板。
类模板的友元函数。
对于struct来说,有一种直接通过统一初始化来赋值的操作;而一旦细分了访问权限,这个操作即被禁止。
构造函数、拷贝构造函数和移动构造函数。
还有深浅拷贝的问题。
同上的部分同上。
同上的部分同上。
同上的部分同上。
在这里要着重讲一下显式部分特化,这是相比于函数模板不同的地方。
通过一系列实验,观察虚继承的效果。
阐释,为什么虚继承是一个关系。
在这里对C++17为止的运算符的优先级、结合性和重载要求作一个整理。
在这里列出0~127的ASCII码表。
这里整理C++中可能用到的相关数学知识。我没必要在主体内容中集中讲它,所以放到这里来。