前言:博客开始记录了一系列C++笔记,但各章节太分散,且编辑格式不是很规范,不便于查看;现把各章节做一个汇总,准备放在一起,方便按知识点查看。汇总版的C++基础笔记包括上、下两部分。
目录:
高级函数
在前面的第五章,介绍了如何实现函数的重载:编写多个名称相同但参数不同的函数,同样,对于类中的成员函数,也可以重载。编译器会根据参数的类型和数据决定调用哪个版本。同样,也成员函数一样,用于初始化对象的构造函数也可以重载。一个类可以有两个或多个构造函数,编译器将根据参数的数量和类型选择正确的构造函数。可以重载构造函数,但不能重载析构函数。因为析构函数的签名总是:名称为类名前加~,且不接受任何参数。
复制构造函数
除了提供默认构造函数和析构函数外,编译器还提供一个默认复制构造函数。每当创建对象的备份时,都将调用复制构造函数。按值将对象传入或传出函数时,都将创建对象的一个临时备份。如果对象是用户自己定义的,就将调用相应类的复制构造函数。
所有复制构造函数都接受一个参数:一个引用,它指向所属类的对象。最好将该引用声明为常量,因为复制构造函数不用修改传入的对象,如:
Tricycle (const Tricycle &trike)
; 在这条语句中,构造函数Tricycle接受一个常量引用,它指向一个现有的Tricycle对象。这个复制构造函数的目标是创建Tricycle对象的备份。
默认构造函数只将作为参数传入的对象的每个成员变量复制都新对象中,这称为浅复制(成员复制)。对于堆内存中的对象,浅复制将一个对象的成员变量的值复制到另一个对象中,这导致两个对象中的指针指向相同的内存地址。而深复制将堆内存中的值复制到新分配的堆内存中。对于浅复制,会出现一种问题,当其中一个对象不在作用域内,将调用对象的析构函数释放内存,而另一个对象仍然指向该内存,这将导致程序出错。解决的办法就是自己定义复制构造函数,并在复制时正确的分配内存。
运算符重载
这章注意讲述了运算符重载,运算符重载定义了将运算符用于对象时应执行的操作,几乎所有C++运算符都可重载。因为一个类的对象不能像内置int型那样进行递增、递减、相加或赋值,也不能使用其他运算符操作它,这就要用到运算符重载了。
要在类中重载运算符,最常见的方式是使用成员函数。方法如下:
returnType operatorsymbol(parameter list)
{
// body of overloaded member function
}
函数名由operator和要定义的运算符(如+或++)组成;returnType为函数的返回类型;parameter list包含0、1或2个参数,具体取决于运算符。
虽然运算符重载很强大,但也有限制:不能重载用于内置类型的运算符;不能改变运算符的优先级和目数(单目、双目或三目);另外,不能创建新的运算符。
使用继承扩展类
C++的特点:封装、继承和多态,这一章,我们就讲继承,如果一个类在现有类的基础上添加了新功能,那么这个类就被称为从原来的类派生而来,而原来的类被称为新类的基类。
派生的语法
在C++中,要从一个类派生出另一个类,可在类声明中的类名后加上冒号,再指定类的访问控制(public、protected或private)以及基类。例如:
class Mammal
{
//哺乳动物类的内容
}
class Dog : public Mammal
{
//狗公有继承了哺乳动物类
}
前面提到那3个访问限定符:public、protected和private。只要有类的对象,函数就能访问该类的所有成员数据和成员函数。在公有继承的情况下,派生类可以访问基类的公有成员和受保护成员,但是不能访问私有成员。
构造函数和析构函数
在创建派生类的对象时,将调用多个构造函数。首先应该明白,派生类对象也是基类对象,因为派生类是通过基类继承而来的,所以,在创建派生类对象时,首先就调用基类构造函数,然后调用派生类构造函数完成对象的创建。在销毁派生类对象时,首先调用派生类的析构函数,然后调用基类的析构函数。这恰恰与创建对象时调用的顺序相反。
重写函数
如果派生类创建了一个返回类型和签名(包括函数名、参数列表和关键字)都与基类成员函数相同的函数,但是提供了新的实现,这就称为重写该函数。派生类可以重写基类的函数,重写函数意味着在派生类中修改基类函数的实现。创建派生类对象时,将调用正确的函数。
重载和重写的区别
重载成员函数时,创建了多个名称相同但签名不同的函数。而重写成员函数时,在派生类中创建了一个名称和签名都与基类函数相同但实现方法不同的函数。
调用基类方法
即使我们重写了基类的方法,仍然可以使用权限定名来调用它。为此,可指定基类名、冒号和方法名,如:已在派生类中重写了move()方法,而现在需调用基类的move()方法,就可以写Mammal::move();
使用多态和派生类
多态即意味着具有多种形态,要使用多态,必须明白前一章所说的派生类的对象也可视为基类对象。将这些派生类对象视为基类对象而调用基类方法时,便使用了多态。
要使用多态,可声明一个基类指针,并将在堆中创建一个派生类对象的地址赋给它。比如(基类:Mammal和派生类:Dog),由于派生类对象也是基类对象,所以下面的代码是合法的:Mammal * pMammal = new Dog
; 然后就可以用该指针对Mammal调用任何成员函数。在调用重写了的函数时,将根据指针指向的对象类型,调用正确的函数。注意:在基类中,被调用的重写的函数必须用virtual关键字修饰,即它是虚函数。不然,当调用该函数时,就会实现基类的方法而不是指针指向的对象的方法,这就达不到多态的效果了。
不能通过基类指针访问派生类特有的方法
如果Dog有成员函数WagTail(),但是Mammal没有,就不能通过Mammal指针来访问它(除非将其转换为Dog指针)。
使用虚成员函数的代价
包含虚成员函数的类必须维护一个虚函数表(v-table),因此使用虚函数会带来一些开销,如果类很小,并且不打算从它派生出其他类,就根本没必要使用虚函数。
问:如果基类的一个函数(Somefunc())是虚函数,且被重载以便能接受一个或两个int参数,而在派生类重写了接受一个int参数的版本,那么通过指向派生类对象的指针调用接受两个int参数的函数时,将调用哪个函数?
答:重写接受一个int参数的版本将隐藏基类中所有同名函数,因此将出现编译出错,指出该函数只接受一个int参数。
使用高级多态
前一章讲到的多态会存在一个问题:将派生类对象赋给基类指针是为了以多态方式使用它,但不应该利用这个指针访问派生类特有的函数,如果这样做了,就会出现编译错误,指出该特有的函数不是基类的成员函数。为此,我们可以将基类指针转换为派生类指针,为了让这种方法可行,可以使用运算符dynamic_cast,它确保转换是安全的。
抽象数据类型
抽象数据类型表示一种概念(如形状),而不是具体的对象(如圆)。在C++中,ADT只能用作其他类的基类,而不能创建其实例。因此,如果将Shape类声明为ADT,便无法创建Shape实例了。
纯虚函数
C++通过提供纯虚函数来支持创建抽象数据类型。纯虚函数是必须在派生类中重写的虚函数。通过将虚函数初始化为0来将其声明为纯虚的,如:virtual void draw() = 0
;
任何包含一个或多个纯虚函数的类都是ADT,不能对其实例化,试图这样做将导致编译错误。将纯虚函数放在类中则说明两点:不要创建这个类的对象,而应该从其派生;务必重写从这个类继承的纯虚函数。在从ADT派生而来的类中,继承的纯虚函数仍然是纯虚的,要实例化这种类的对象,必须重写每个纯虚函数。
问:向上提升功能是什么意思?
答:这指的是将共享功能向上提升,将其放到基类中。对于多个类都需要的函数,最好将其放在一个合适的基类中。
使用链表存储信息
链表是一种数据结构,由连接在一起的小容器组成。在这里,容器是类,它们包含要存储在链表中的对象。这些容器称为节点。链表中的第一个节点称为头节点,最后一个节点称为尾节点。链表有三种基本形式,从最简单到最复杂依次为:单链表、双链表和树。
在单链表中,每个节点都指向下一个节点,但不指向前一个节点。要查找特定的节点,从链表头开始,逐个节点往下找。双链表可以向前和向后移动到下一个节点和前一个节点。树是一种由节点组成的复杂数据结构,每个节点都可能指向多个节点。链表由节点组成。节点类本身是一个抽象类,将使用3个子类(头节点类、内部节点类和尾节点类)来完成工作。链表包含一个头节点和一个尾节点,它们负责管理链表的组成部分,还包含零或更多个内部节点。内部节点用于记录存储在链表中的实际数据。注意,数据和链表是不同的概念,可在链表中存储任何类型的数据,连接在一起的不是数据而是存储数据的节点。
问:在数组管用的情况下为何要创建链表?
答:数组的大小是固定的,而链表的大小可在运行阶段动态的调整。
使用特殊的类、函数和指针
静态成员数据
静态成员变量,被关键字static修饰,不同于其他成员变量,它是同一个类的所有实例共享的变量,它们是全局数据(可供程序所有部分使用)和成员数据(通常只供一个对象使用)的折衷。可将静态成员视为属于类而不是对象,通常,成员数据是每个对象一个,而静态成员数据是每个类一个。
静态成员函数
静态成员函数也和静态成员变量类似:它们不属于某一个对象而属于整个类,因此,不通过对象也能调用它们。注意:静态成员函数没有this指针。因此,不能将它们声明为const。另外,由于在成员函数中是通过this指针来访问成员数据变量的,因此,静态成员函数不能访问非静态成员变量。
友元类和友元函数
当我们想要创建成对的类,它们需要能够彼此访问对象的私有成员,但又不想这些信息变成公有的,即我们想将一个类的私有成员数据或函数暴露给另一个类,就必须将其声明为友元类。这扩展了类接口,使其包含友元类。友元关系不能传递,不能继承,也不可交换,即将class1声明为class2的友元并不能让class2成为class1的友元。也可只将成员函数声明为友元函数。
函数指针
大家都知道,数组名是指向数组第一个元素的常量指针,而函数名也是指向函数的常量指针。我们可以声明指向函数的指针,并使用该指针来调用相应的函数。函数指针必须指向有合适返回类型和签名的函数。如:long (*funcPtr) (int)
;在这句声明中,funcPtr被声明为一个指针,它指向接受一个int参数且返回类型为long的函数。*funcPtr
两边的括号是必不可少的,因为int两边的括号的优先级比间接运算符(*)更高。下面是它们的区别:
long* func(int); //将fun()声明为接受一个int参数且返回类型为long指针的函数
long (*funcPtr) (int); //将funcPtr声明为一个函数指针,它指向的函数接受一个int参数且返回类型为long
成员函数指针
要创建成员函数指针,可使用与创建非成员函数指针相同的语法,但需要包含类名和作用域运算符(::)。因此,如果pFunc指向类Shape的一个成员函数,它接受两个int参数且返回类型为void,那个pFunc的声明如下:void (Shape::*pFunc) (int,int)
;成员函数指针的用法和函数指针相同,只是它要通过相应类的对象来调用。注意:我们不需要对pFunc调用delete,因为它是一个指向代码的指针,而不是指向堆中对象的指针。
使用C++0x新增的功能
空指针常量
在前面第十章有讲到,使用指针时一定要给它赋值,这很重要,因为未初始化的指针可能指向内存的任何位置,这称为野指针。为了避免这种危险,创建时应将空值(0或NULL)赋给指针。如:int *pBuffer = 0
; int *pBuffer = NULL
;这两条语句等效。NULL是一个预处理器宏,会被转换为0(整型)或0L(长整型)。C++0x新增表示空指针的关键字nullptr,如:int *pBuffer = nullptr
;nullptr不会隐式地转换为整数,但可能隐式地转换为布尔值。当隐式转换为布尔值时,nullptr将被转换为false。
编译阶段常量表达式
C++0x新增的常量表达式,是使用关键字constexpr实现的:
constexpr int getCentury()
{
return 100;
}
常量表达式的返回值不能为void,并且包含代码return expression。返回的表达式只能包含字面值、对其他常量表达式的调用或被声明为constexpr的变量。如:定义一个constexpr变量:
const int century = 100;
constexpr year = 2011 + century;
自动确定类型的变量
C++0x新增了关键字auto,让编译器根据首次赋给变量的值来确定其类型。如:
auto index = 3;
auto gpa = 2.25F;
auto rato = 500/3.0;
它也适用于函数的返回值,如:auto score = calculateScore()
;变量score的数据类型将为函数的返回类型。
关键字auto用于定义类或结构的成员变量,除非它是静态成员。也可以用一个auto关键字定义多个变量,条件是这些变量的数据类型要相同。
但是它也有一些限制,不能使用auto来声明数组的类型,也不能将其作为函数参数或函数的返回值类型。
创建模板
上一章是通过一个案例分析来讲述怎样做面向对象分析和设计,于是上一章我就没写出来,因为其中要讲的细节太多了。这一章,讲述了模板类的创建,模板可以让你创建通用类,通过将类型作为参数传递给模板,可创建其实例。
模板的定义
如要声明模板类List,可使用关键字template,如下所示:
template <class T> // declare the template and the parameter
class List //the class being parameterized
{
public:
List();
// full class declaration here
};
所有模板类的声明和定义都以关键字template打头,接下来是模板的参数,它们随模板实例而异。在这个例子中,使用了关键字class和标识符T。关键字class表明这个参数为类型;在模板定义的其他地方,都将使用标识符T来表示参数化类型。在这个类的一个实例中,可能使用int替换所有T,而在另一个实例中,可能使用Cat类替换所有T。
处理异常和错误
要想写好一段程序,会考虑很多问题,比如:程序的逻辑性,程序的语法问题和程序运行阶段程序的健壮性问题等。区分Bug、逻辑错误和异常至关重要。Bug是由于程序员犯错引起的;逻辑错误是由于程序员对问题解决问题的方式不了解引起的;最后,异常是由于不常见但可预见的问题(如用户错误输入、内存或硬盘空间等资源耗尽)引起的。
处理意外情况
程序员可通过设计审核和详尽测试来发现逻辑错误。但是,异常不同,你无法消除异常情况,只能为此做好准备,相应的措施有:崩溃、通知用户并妥善退出、通知用户,并让用户尝试恢复并继续执行、采取正确的措施,在不影响用户的情况下继续运行。
异常
在C++中,异常是一个对象,从发生问题的代码传递给处理问题的代码。发生异常被称为引发,处理异常被称为捕获。
对于可能发生问题的代码,应将其放在try块中。try块是一个用大括号括起来的代码块,其中的代码可能引发异常。catch块紧跟在try块后面,负责对异常进行处理。如:
try
{
someDangerousFunction();
}
catch(outofMemory)
{
//take action to recover from low memory conditon
}
catch(fileNotFound)
{
//take action when a file is not found
}
规范代码的编写
1、大括号
1)匹配的大括号应水平对齐。
2)在定义和声明中,最外面的大括号应在最左边;内部的语句应该缩进;其他所有的大括号都应与相关联的语句左对齐。
3)对于特别长的代码块,应将注释放在代码块的右大括号后面,并指出代码块的用途。
4)大括号应单独占一行。
2、长代码行
1)确保不用水平滚动就能看到整行代码。超出右边界的代码易被忽略,且水平滚动很烦人。将一行代码分成多行时,对后续行进行缩进。尽量在合理的地方分行,将运算符放在前一行末尾(而不是下一行开头),这样可以清晰的知道,该行并不完整,后面还有其他代码。
2)制表符应包含三四个空格,确保编辑器能够将制表符转换为这样的长度。
3、switch语句
按如下方式缩进switch语句,可节省水平空间:
switch(variable)
{
case valueone:
actionone();
break;
case valuetwo:
actiontwo();
break;
default:
//bad action
break;
}
4、程序文本
可使用下面的技巧让代码易于阅读。
1)使用空白提高可读性。
2)不要在对象、数组名和运算符(.、->、[ ])之间使用空格。
3)单目运算符与其操作数相关联,因此不要在它们之间添加空格,但是可以在操作数一边添加空格。单目运算符包括!、~、++、–、*(用于指针)、&(取地址)和sizeof。
4)双目运算符两边都应有空格,双目运算符包括+、=、*、/、%、》、《、<、>、==、!=、&、|、&&、||、?:、+=等。
5)不通过省略空格来指示优先级,如(4+ 3*2)。
6)在逗号和分号后面加空格,但在前面不加。
7)圆括号两边不应该有空格。
8)用空格将关键字(如if)分开,如 if (a==b)。
9)使用空格将单行注释的内容同//分开。
10)将指针或引用指示符紧靠类型名,而不是变量名。如: char* foo; int& theInt;
11)不在一行中声明多个变量,除非它们彼此相关。
5、标识符命名
1)标识符名称应足够长,以便具有描述性。
2)避免意义不明确的缩写,应将含义拼写出来。
3)仅当简短性可提高代码的可读性或用途十分明显不需要描述性名称时,才使用短名称(i、p、x等)。
4)变量名的长度应与其作用域相称。
5)函数(或方法)名通常为动词或动词-名词短语,如secrch()、reset()、findParagraph()。变量名通常为抽象名词,可以带一个附加名词,如count、state。布尔变量应相应地命名,如windowIconized、fileIsOpen。
6、名称的拼写和大写
1)标识符应采取一致的命名方式,在合适的情况下混合使用大小写。函数、类、typedef、结构、数据成员和局部变量的名称应采用首字母小写
2)枚举常量应以表示枚举类型缩写的小写字母开头。如:
enum TextStyle
{
tsPlain,
tsBold,
tsItalic,
};
7、注释
1)尽可能使用C++单行注释(//),而不是多行注释(/* */)。
2)高级注释比处理细节更重要。要增加价值,而不是复述代码。将重点放在函数和代码块的语义上:指出函数的功能、副作用、参数类型和返回值;描述做出(或没有做出)的所有假设。
3)使用采用合适标点符号和大小写的完整的句子,不然到后面自己也看不懂。
4)在程序、函数和头文件模块开头添加注释,指出模块的用途、输入、输出、参数、最初编写它的程序员以及所做出的修改(包括日期和程序员)。
5)使用空行来帮助读者理解所发生的事情,将语句分成逻辑组。
8、设置访问权限
1)总是使用public:、private:和protected:,不要依赖于默认访问设置。
2)先列出公有成员,其次是保护成员,然后是私有成员。在方法后面集中列出数据成员。
3)在合适的地方首先列出构造函数,然后是析构函数;集中列出同名的重载方法;尽可能集中列出存取器函数。
4)考虑按字母顺序排列每组中的成员函数和成员变量。
5)虽然重写函数时关键字virtual是可选的,但是应尽可能使用它。
9、类定义
1)尽可能让成员函数定义的排列顺序与声明顺序一致,这让函数更容易找到。
10、包含文件
最大限度地减少在头文件中包含的文件。最理想情况是,只包含基类的头文件,其他必须包含的文件是声明类成员对象的头文件。
11、使用const
在合适的地方使用const:参数、变量和成员函数。