第二章 表达式
C++兼容C子集(目前为C89)中的5种基本数据类型:分别为char、int、float、double和void。
C++在此基础上又增加了bool和wchar_t这两种基本数据类型。
关于标准对于数据类型的规定,最重要的是要理解C和C++都仅规定了每种数据类型的最小表示范围,而不是它的字节长度。
C中标识符可以是任意长度,对于具备外部链接(external linkage)的标识符,至少前六个字符是有效的;对于非外部连接的标识符,至少前32个字符是有效的。
C++中对标识符的长度没有限制,并且标识符的至少前1024个字符是有效的。
变量可以在三个地方声明:函数内部,函数参数定义处和所有函数的外部,分别对应局部变量、形式参数和全局变量。
存储限定符:extern、 static 、 register 、 auto
extern
C/C++定义了三种链接:外部链接、内部链接和无链接。
通常,函数和全局变量具有外部链接,即他们对于构成程序的所有文件都是可用的;被声明为静态(static)的全局对象具有内部链接,即仅在其被定义的文件中是可知的;局部变量没有链接。
extern的主要用途是来声明一个对象在程序中的其他地方有外部链接。
static
静态局部变量:编译器为其分配永久的存储区。
静态全局变量:编译器创建一个仅在声明它的文件中可知的全局变量。
C++中鼓励使用namespace来实现static的第二项功能。
C/C++中都定义了宽字符wchar_t,要指定一个宽字符常量,在字符前面加L,例如 wchar_t wc=L'A';
C中,wchar_t是在头文件wchar.h中定义的,不是基本类型,而是typedef;在C++中,wchar_t是基本类型的。
C/C++中都没有指定表达式中子表达式的求知顺序。
第四章 数组和以null结束的字符串
如果函数的某个参数是一个一维数组,可以通过以下3种方式声明,具有相同的效果:
作为指针: void func( char * arr);
作为固定长度的数组 void func( char arr[16]);
作为不定长度的数组 void func( char arr[]);
在传递多维数组参数时,必须声明出了最左维以外的所有维。
子聚集分组(subaggregate grouping):在初始化一个多维数组时,可以在每一维的初始化部分周围添加括号,例如
int array[10][2]= { {1,2},{3,4},{5,6} };
在使用子聚集分组时,如果没有为给定的每组提供足够的初始化,则剩余的成员则
自动设置为0值。
第五章 指针
所有指针运算都与其指向的类型有关。
两个指针变量或是一个指针变量与一个整数之间的算术运算,是以其类型元素为单位进行的。
在基于指针来访问数组元素时,尽管标准数组下标符号有时更容易理解,但是指针运算更快些。
在C中,void *类型的指针被自动转换为赋值语句左边的指针类型;而在C++中,这种自动转换并不存在,需要显示转换。
第六章 函数
main()函数中的agrc和argv只是传统用法,其名字可以是任意的,用户可以任意选用自己喜欢的名字来表示传递给main的参数名。
C和C++在处理无参数的函数存在
细微而重要的区别:
C++中,空参数列表通过在原型中不提供任何参数简单的表示。
而在C中,这样的原型有不同意味——一个空参数列表只是表示未给出参数信息,当编译器遇到这种情况,会认为这个函数可能有多个参数,也可能没有参数。因此,若要声明无参数的函数,需要在参数列表中使用void。
第十章 预处理和注释
在预处理这个方面,C和C++的区别在于他们依赖预处理的程度。在C语言中,每条预处理命令都是必需的,而在C++中,由于更新、更好的语言元素的出现,某些预处理特征变成多余的了。事实上,C++的一个长期设计目标是完全消除预处理器。
每个预处理命令必需独占一行,在一行内出现多个预处理命令是无效的
#define
字符串中的宏在预处理过程中不进行替换。
另外,在带参数的宏定义中,如果某个形式参数的前面有符号#,或是之前或后面有一个##符号,则在预处理过程中时不对实际参数进行嵌套替换和扩展。
#error error-message
#error指令强迫编译器停止编译,主要用于程序调试。注意格式中的error-message不加双引号。
#line
#line指令改变内置宏__LINE__和__FILE__的内容,
预定义的宏
C++规定了六个内嵌的预定义宏:
__LINE__: 当前行在源文件中的位置
__FILE__: 当前源文件名
__DATE__:源文件被编译时的日期
__TIME__:源文件被编译时的时间
__STDC__:若__STDC__已定义,则编译器将仅接受不包含任何非标准扩展的标准C/C++代码。
__cplusplus:定义为一个包含至少六位的数。
C语言定义了其中前5个宏。
第十一章 C++概述
C++是编译型语言,因此既支持编译时的多态,也支持运行时的多态。
函数重载要求函数的参数个数和类型必须不同,两个函数仅在返回类型上不同是不够的,必须在参数数量或类型上不同,否则无法提供足够的信息让编译器判断使用哪个函数。
第十二章 类和对象
类成员的限制:非静态成员变量没有初值;成员变量不能声明为auto,extern 或register
C++中,class与struct的唯一区别在于,默认情况下,class的所有成员都是privarte的,而struct的所有成员都是公有的。
友元函数
友元函数可以访问类的所有privete和protected成员。
友元函数的两个重要限制:
1.基类的派生类不继承友元函数
2。友元函数不能有存储类标识符,即static或extern。
友元类
一个类可以是另一个类的友元,这样友元类和它的所有成员函数可以访问其他类中定义的private成员。 友元类较少被实际使用。
内联函数
内联函数必须在调用之前已经声明或定义。
内联函数中不能存在复杂的语法结构,如switch和while等,编译器遇到这样的情形会按照普通函数来处理。
与register限定符一样,inline对编译器来说是一种请求,而不是强制性的命令。
通常,编译器不能内联一个递归函数。
在类中定义内联函数
如果一个函数是在类声明中定义的,那么它将自动的转换为内联函数(在可能的前提下)。
在专业水平的C++代码中,在类声明之外定义短成员函数是非常罕见的。
类的构造和析构函数也可以是内联的。
构造函数
如果类的构造函数仅有一个参数,那么存在一种特殊的对象初始化方法
例如
class X{
int a ;
X(int j) { a=j;}
}
int main ()
{
X obj=99;
}
这样的初始化方法是合法的。
即只要类存在单参构造函数,就隐式的创建了一个从构造函数的参数类型到类的转换。
再次申明,这种初始化方法仅适用于只带一个参数的构造函数。
静态类成员
在创建第一个类对象之前,所有静态变量都被初始化为0。
在类中声明一个静态数据成员时,并没有对其进行定义(分配存储空间),相反,必须在类外对静态数据成员提供全局定义,通过作用域限定符为其分配内存。
需要理解,类定义只是逻辑结构,而不是物理存在。
通过使用静态成员变量,实际上可以代替全局变量。全局变量给OOP带来的问题就是它们几乎总是违背封装原则。
静态成员函数的限制
静态成员函数只能引用这个类的其他静态成员,不允许访问类中的非静态成员。
静态成员函数没有this指针。
同一个函数不能有静态和非静态两种版本。
静态成员函数不可以是虚函数。
静态成员函数不可以声明为const或volatile。
实际上,静态成员函数的应用是有限的,好处是在创建任何对象之前可以"预初始化"静态成员变量。
嵌套类
可以在类中定义另一个类。由于一个类声明定义了一个作用域,嵌套类只在封闭类的作用域中有效。
局部类
类可以在函数中定义。
当在一个函数内部声明一个类时,这个类只为该函数所知,在函数外它是不可知的。
局部类的限制:
1。所有成员函数必须在类声明中定义,局部类无法访问其所属的函数的局部变量。
2。不能在局部类里声明任何静态变量。
第十三章 数组、指针、引用和动态分配运算符
对象数组的初始化
假设用户定义的类有明确声明的构造函数(且是带参数的),那么如果在定义对象数组时,若不提供参数,则无法通过编译。
例如有如下定义的用户类cl:
class cl{
int i;
public:
cl (int j) { i=j; }
};
如果声明如下数组的话,编译时会报错
cl array[10];
原因是该数组声明暗示类cl有一个无参数的构造函数,然而我们从cl的类声明中看到并不存在这样的个构造函数,所以报错。
解决该问题的方法是在类cl中重载一个无参数的构造函数。
this 指针
1.友元函数不是类的成员,因此被调用时不传递this指针。
2.类的静态成员函数没有this指针。
指向派生类的指针
基类指针可以指向任何派生类对象,反过来却不成立。
然而, 即便 基类指针指向一个派生类对象,也只能通过该指针访问基类成员,不能访问派生类独有的成员。
对象指针与基本类型的指针运算类似,都是以指针类型为单位进行的。
指向类成员的指针
C++允许一种特殊类型的指针,它指向类的成员,而不是指向某个对象中该类成员的实例。这种指针称为指向成员的指针。
指向成员的指针不同于普通的C++指针,它知提供成员在其所属类对象中的
偏移量。
由于成员指针不是真正的指针,因此不能使用运算符*和->,而必须使用特殊的指针运算符.*和->*。
注意,指向成员的指针和指向对象元素的特定实例的指针是不同的,前者代表的是相对位移,而后者则是内存地址。
引用
引用是一个隐含的指针,有三种使用方式:函数参数、函数返回值、独立引用。
引用参数:
引用最重要的用途,或许就是允许创建自动按引用而非传值方式进行参数传递的函数。
返回引用:
函数可以返回引用,这对于允许函数出现在赋值语句的左端有令人吃惊的效果。
独立引用:
类似于指针,可以使用基类引用来引用派生类对象。
引用的限制
1.不能引用其它引用,即不能获得引用的地址。因此,不能创建引用数组,不能创建引用指针,不能引用位域。
2.如果引用变量不是类成员、函数参数或返回值,在声明引用变量时必须初始化。
动态分配运算符
C++标准规定new在失败时将生成bad_alloc异常,这个异常是在头文件<new>中定义的。
可以通过在new语句中类型名后面放一个初值来把分配的内存初始化。
用new分配数组时,不能赋初值。
在用new分配对象数组时,需要注意,由于new不支持为数组初始化,所以需要确保类中存在无参数的构造函数。
nothrow new
在标准C++中,在new执行失败后,可以选择让new返回控制而不是抛出异常,这在用new代替对malloc()的调用时,是很有用的。
形式如下: p_var = new (nothrow) type;
placement new
new的一种特定形式,可用于指定分配内存的另一种方法,在重在new运算符时,是很有用的。
形式如下: p_var =new ( arg-list) type;
其中,arg-list是用逗号分隔的值列表,用于传递给new的重载形式。
第十四章 函数重载、拷贝构造函数和默认变元
或许最常见的重载函数是类构造函数,或许重载构造函数中最重要的形式是拷贝构造函数。
拷贝构造函数只应用于初始化。
初始化发生在三种情况下:
1.用一个对象初始化另一个对象,例如对象声明。
2.把一个对象的拷贝传给一个函数时(参数传递)。
3.当生成临时对象时(函数返回值)。
参数默认值
参数默认值在函数声明或定义中
只需指定一次即可,重复指定会编译报错。
参数默认值应该遵循“右对齐”的原则。
函数重载和二义性
造成二义性的主要原因是C++的自动类型转换。
另一个导致二义性的原因是重载函数中使用默认值。
第十五章 运算符重载
运算符函数使用关键字operator创建。
运算符既可以是一个类的成员,也可以不是一个类的成员;但非成员运算服函数几乎总是该类的友元函数。
成员运算符函数
运算符重载的限制:
用户不能改变运算符的优先级,也不能改变运算符的操作数的个数;
除了函数调用操作符之外,运算符函数不能有默认变元;
以下运算符不允许重载:. 、::、 .*、?。
除了运算符=之外,其它运算符函数可以被派生类继承。
友元函数的运算符重载
使用友元重载可增加重载运算符的灵活性。
第十六章 继承
在派生类与基类的访问限定符这个问题上,class默认情况为private,而struct默认情况为public。
当基类的限定服为public时,基类的所有公有成员将成为派生类的public成员,基类的所有protected成员将成为派生类的protected成员,而基类的private成员总是为基类专有,不能被派生类访问。
当基类的限定符为private时,基类的公有和保护成员将成为派生类的private成员。
protected 继承
当继承基类所使用的限定符为protected时,基类的所有public和protected成员讲成为派生类的保护成员。
将参数传递给基类构造函数
一般来说,派生类的构造函数不仅要声明自己需要的参数,还要声明基类所需要的参数。
另一方面,将某个参数传递给基类并没有阻止派生类访问该参数。
准许访问
某些清况下,可能需要将派生类的某个成员恢复至其在基类中的原始访问声明。可行的方法包括:
1.使用using declaration
2.派生类内部使用访问声明。
访问声明的一般形式:
base-clase::member;
注意,访问声明
只能恢复成员在基类中原本的访问权限,而不会提升或降低某成员的访问状态。
C++尽管支持访问声明这种机制,但是并不提倡该做法;相反,标准建议通过using declaration来达到同样的效果。
虚基类
在继承多个基类时,可能在派生类中引入二义性。
例如,若多个基类中有定义相同的成员,派生类在访问这些成员时会出现二义性问题。
当两个或多个对象源自一个公共基类时,可以通过在继承基类时把该基类声明为virtual来避免一个源自这些对象的对象中出现多个基类副本。
普通基类和虚基类的唯一区别在于:当一个对象多次继承基类时将出现何种情况。如果是用虚基类,则在该对象中只出现一个基类,否则,将出现多个基类副本。
第十七章 虚函数与多态性
C++中,编译时的多态性是通过函数重载和运算符重载来实现的,运行时多态是通过继承和虚函数来实现的。
虚函数在基类内部声明并且被派生类重新定义,方法是在函数声明之前加上关键字virtual。
虚函数之所以重要,之所以能够支持运行时的多态,就在于当通过指针对其访问时虚函数的表现。
虽然可以通过使用对象名和点运算符以一般的方式调用虚函数,但只有通过基类指针(或引用)访问时才能体现运行时的多态性。
虚函数不能是类的静态成员
虚函数不能是类的友元
构造函数不能是虚函数,但析构函数可以是虚函数。
虚属性的
继承
当继承虚函数时,其虚属性也被继承下来。换句话说,无论虚函数被继承了多少次,仍然是虚函数。
纯虚函数
当虚函数变为纯虚函数时,任何派生类必须给出自己的定义,若派生类未覆盖纯虚函数,则会有编译错误。
抽象类
至少包含一个纯虚函数的类称为抽象类。
不能创建抽象类的对象,然而可以创建指向抽象类的指针和引用,从而支持运行时的多态性。
早期绑定和后期绑定
早期绑定是编译时发生的事件。从本质上讲,如果编译时知道调用某个函数需要的所有信息,就会发生早期绑定,例如正常的函数调用、函数重载以及运算符重载。早期绑定的主要优点是高效。
后期绑定是指在运行时才确定的函数调用。可以利用虚函数来实现后期绑定。后期绑定的主要优点是灵活,但是速度变慢。
第十九章 异常处理
异常处理基础
C++异常处理依赖于3个关键字:try ,throw, catch。
如果抛出 异常却 没有合适的catch能够处理,程序将会异常终止。在抛出的异常未被处理时,会调用标准函数库terminate()。默认情况下,terminate()会调用abort()终止程序,但是用户也可以指定自己的终止处理程序。
一般来说,catch语句中的代码总是要努力通过适当的操作来纠正某种错误,如果错误可以纠正,程序将继续执行catch语句后面的语句;否则,catch语句块将通过调用exit()或abort()终止程序。
只有当catch捕获到异常时,相应的代码才会得到执行,否则程序的执行将直接跳过catch语句块。
抛出的异常可以是任何类型,包括用户创建的类。事实上,在实际使用的程序中,大多数抛出的异常类型都是类而不是内置类型。
给异常定义类的最常见原因是要创建一个描述所出现错误的对象,异常处理程序可以利用这个对象来处理发生的错误。
一般来说,多个catch语句按照他们在程序中出现的循序被检测,只有与异常相匹配的语句才会被执行,所有其他的catch语句块将被忽略。
处理派生类异常
由于基类的catch语句同样可以匹配抛出的派生类异常,因此当试图捕获设计基类及其派生类的异常类型时,必须仔细安排多个catch语句块的顺序。
如果既要捕获基类异常,又要捕获派生类异常,则应该在catch序列中首先放置捕获派生类的catch语句块,否则基类的catch语句将捕获所有的派生类异常。
异常处理选项
想要捕获所有异常,只需采用下面的catch语句:
catch(...)
{
}
catch(...)一种很好的应用,就是将其作为一组catch语句的最后一条语句,来提供一种默认处理机制,类似于switch语句中的default标签。此外,通过捕获所有异常,可以防止由于某个未处理的异常而引起程序异常终止。
异常规格
可以限制某个函数所能抛出的异常类型,一般形式:
ret-type func-name (arg-list ) throw(type-list)
{
//...
}
这样,只有包含在type-list中的数据类型可以被函数抛出,如果type-list为空,则函数无法抛出任何异常。
若函数试图抛出一个不在type-list中的异常类型,则会调用标准库函数unexpected()。默认情况下,它会调用abort(),程序异常终止。
一个函数只能被限制为抛回给调用它的try语句块的异常类型,理解这一点非常重要。也就是说,只要异常可以在函数内部被捕获,那么该函数中可抛出任意异常类型。以上的限制只适用于在函数外抛出异常的情况。
异常
再次抛出
最常见的原因是允许多个处理程序访问该异常。
但一个异常被再次抛出时,该异常将不会被当前catch语句再次捕获。
terminate() & unexpected()
#include <exception>
void teminate();
void unexpected();
1.异常处理子系统找不到相匹配的catch语句时,会调用terminate()。
2.最初没有异常抛出,而程序却试图再次抛出一个异常时,会调用terminate()。
3.一般来说,当没有其他异常处理程序可用时,terminate()就是最后一种手段。
默认情况下,terminate()调用abort().
当函数试图抛出一个不再throw列表中的异常时,会调用unexpected().默认情况下,unexpected()会调用terminate()
为了改变终止(terminate)处理程序,可以使用set_terminate()。
为了改变意外(unexpected)处理程序,可以使用set_unexpected()。
uncaught_exception()函数
原型: bool uncaught_exception();
如果一个异常被抛出而尚未被捕获,则该函数返回true;一旦异常已被捕获,该函数返回false。
第22章 运行时类型标识和强制转换运算符
RTII:Run Time Type Identifier
C++利用对象的层次性、虚函数以及基类指针,实现了运行时的多态性
要获得一个对象的类型,可以调用typeid()函数,这需要包含头文件<typeinfo>
typeid的常见形式:typeid(object)
其中,object可以是任何类型的对象,内置类型或用户自定义类均可。
typeid()返回一个对type_info类对象的引用,该类描述了对象类型。
typeid()最重要的用途是确认一个基态指针所指对象的具体类型。
当typeid()应用于一个多态类型的基类指针时,指针所指对象的类型将在运行时确定。
当typeid()应用月一个非多态的类层次结构的指针,总是获得基类的类型。
typeid()的另一中调用形式是将类型名作为参数。
强制转换运算符
C++中定义了5个强制转换运算符,除了从C继承而来的传统形式的强制转换运算符外,另外4个是dynamic_cast、const_cast、reinterpret_cast和static_cast。
dynamic_cast
这或许是最重要的一个强制转换运算符了。
dynamic_cast执行一个运行时转换,该转换验证一个强制转换的有效性
一般形式: dynamic_cast<target-type> (expr)
其中,target-type指定强制类型转换的目标类型,expr是被强制转换为新类型的表达式。目标类型必须是一个指针或是一个引用,被强制转换的表 达式的值必须是一个指针或是引用。因此,dynamic_cast可以用来将一种指针类型强制转换为另一种指针类型,或是将一种引用类型强制转换为另一种 引用类型。
dynamic_cast的目的是执行
多态类型的强制转换。
如果转换失败,在转换涉及到指针时,dynamic_cast的生成值为空;在转换涉及到引用时,则会抛出一个bad_cast异常。
const_cast
const_cast运算符用于在一个强制类型转换操作中显式的重载const和/或volatile。
一般形式: const_cast<type>(expr)
其目标类型必须与源类型相同(const或volatile属性的改变除外)。
const_castd的最常见用途是去除const属性。
需要强调的是,是用const_cast删除const属性是一种具有潜在危险的功能,使用时要格外小心。
另外,只有const_cast可以删除const属性,无论是dynamic_cast、static_cast或是reinterpret_cast,它们都不能改变一个对象的const属性。
static_cast
static_cast运算符执行非多态转换,不执行运行时检查。
一般形式: static_cast<type>(expr)
static_cast运算符本质上是传统形式的强制转换运算符的替代品。
reinterpret_cast
reinterpret_cast运算符将一种类型转换为一个不同的类型,例如将指针转换为整数,整数转换为指针等;也可以使用它强制转换不兼容的指针类型。
第23章 名字空间、转换函数和其它
引入namespace最明显的受益者或许是C++标准库,由于namespace的引入,C++库现在定义在名为std的名字空间中,从而减少了发生名字冲突的可能性。
using语句的两种形式
using namespace space-name;
using space-name:member-name;
添加一个名字空间到全局空间并不会覆盖另一个名字空间的内容。
匿名名字空间
namespcae {
//declarations
}
匿名名字空间可以使你创建一些唯一的标识符,这些标识符只在该文件的作用域内为程序所知。也就是说,在含有匿名名字空间的文件内,这个匿名名字空间的成员可以不受限制的直接使用,但在这个文件外部,这些标识符是不可见的。
匿名名字空间的存在,实际上取消了static存储类修饰符存在的必要性。
其它名字空间选项
一个名字空间可以被拆分散步在几个文件中,甚至可以在同一个文件内被分割。
一个名字空间必须在所有其他作用域之外被声明,这意味着不能在一个函数内部定义名字空间。然而有一个例外情况,即一个名字空间可以被嵌套在另一个名字空间之中。
一般来说,大多数中小型程序不需要创建名字空间,然而如果要创建可重用的代码库或是最大程度的确保程序的可移植性,应该考虑把代码包装在一个名字空间内。
std名字空间
老式的.h头文件将文件的内容放入全局名字空间,而新式的头文件把文件内容放入std名字空间。
转换函数
某些情况下,或许需要在一个包含其他类型数据的表达式中使用一个类的对象。重载运算符可以提供解决方案。但是,有时需要的仅仅是把某种类类型转换到目标类 型的简单类型转换。为了处理这种情况,C++允许创建定制转换函数。转换函数可以将创建的类转换为与表达式中其他内容兼容的类型。
一般形式: operator type () { return value;}
转换函数不能有参数,必须是类的成员函数。它可以被继承,也可以是虚函数。
在类对象与内置类型混在一起时,转换函数可以提供更为自然的语法。
const成员函数
类成员函数可以被声明为const,这使得this可以被当作一个const指针,从而使得该成员函数无法修改调用它的对象。
mutable成员变量
若有mutable关键字修饰类成员变量时,则该成员变量可以被const成员函数修改,即mutable覆盖了const
volatile成员函数
类成员函数可以被声明为volatile,这使得this被当作一个volatile指针。
explict构造函数
限定符explict只作用于构造函数,这样的构造函数不执行自动转换(在参数为1的情况下发生)。
类成员初始化语法
类中的const成员变量必须被初始化,并且在初始化后不能被赋值。
为了解决const成员变量初始化的问题,C++提供了另一种成员初始化语法,与调用基类构造函数所用的语法类似。
成员初始化语法特别适用于没有默认构造函数的类的类成员。
连接说明
可以在C++中指定如何将一个函数链接到你的程序中。默认情况下,这些函数将作为C++函数被链接,然而,使用链接说明,可以链接一个不同类型语言的函数。
一般形式: extern "language-name" function-prototype;
可以使用一下形式链接多个函数
extern "language-name"
{
prototype1;
prototype2;
}
C与C++的区别
C中,无参函数必须使用void关键字;C++中,无参函数中void是可选的。
C中,不能获得register变量的地址;C++中,可以获得这个变量的地址。
C中,如果一个函数声明语句中没有说明返回类型,则返回类型假设为int。这种默认规则不再适用于C++。