C++ Primer笔记
指针和typedef:
typedef string *pstring
const pstring cstr;
等价于
string * const cstr
cstr为const指针,而不是cstr指向const string,typedef不是简单的文本扩展,声明const pstring时,const修饰的是pstring,pstring是指针,所以const修饰的是指针而不是指针所指的对象。
在switch内部只可以在最后一个case或default标号中定义变量,这个规则是为了避免出现代码跳过变量的定义和初始化的情况,如果要为某个case定义爆裂,可引入块语句({})。
指向指针的引用:*&,如int *&i。
数组形参:
当数组作为函数的参数传递时,应将数组名作为实参,函数声明中的形参可以有三种形式:
void pringArray(int *);
void printArray(int arr[]);
void printArray(int arr[10]);
这三种形式是等价的,因为最终都会被转化为第一种形式,数组名为指向第一个元素的指针,第三种形式会引起误解,因为其会转化为指针,并无法保证数组的长度。
数组形参可以声明为引用,这样的话,编译器不会将数组实参转化为指针,而是传递数组的引用本身,数组的大小成为形参和实参的一部分,编译器检查数组实参的大小与形参的大小是否匹配,如:void pringArray(int (&arr)[10])可保证数组的长度为10.
&arr两边的括号是必须的,因为[]的优先级更高。
int &arr[10] //an array of 10 references
int (&arr)[10] //a referenceto to an array of 10 ints
类的const成员函数:
类的每个非static成员函数都有一个额外的、隐含的形参this,通过在形参表后加const可使其成为const成员函数,此时this作为const类型,不能改变所指对象的状态。const对象、指向const对象的引用或指针只能访问const成员变量和const成员函数,因为非const成员可能会改变对象的状态。但有一个例外,声明为mutable的类成员为可变数据成员,表示可以被修改(甚至在const成员函数内),所以其不可能为const,即使其为const对象的成员。可以修改static成员,因为static成员不属于个别对象而属于类。
局部作用域中函数的声明会屏蔽其外围作用域中的所有同名函数,无论其形参列表是否相同,因为编译器以名字查找函数调用,当在一个作用域中找到名字后就不再在其外围作用域中继续查找。
仅当形参是引用或指针时,才能只基于形参是否为const实现重载,当定义这种形式的重载时,因为不能将const引用或指针传递给非const引用或指针形参(非const可能改变所引向或指向的对象的值),所以const实参会调用const版本;非const引用或指针既可初始化非const引用或指针,也可初始化const引用或指针,但初始化const型时会引发转换,所以非const版本更匹配,非const实参会调用非const版本。
指向函数的指针:
函数类型由其返回类型与形参表确定,与函数名无关:
int (* func)(int,int);
这个语句声明一个指向函数的指针func,它所指向的函数带有两个int参数,返回一个int。
func两侧的括号是必须的:
int *func(int,int);
表示声明一个函数,拥有两个int参数,返回int *
用typedef简化函数指针的定义:
typedef int (*func)(int,int);
该定义表示func是一个指向函数的指针类型的名字,在要使用这种函数指针类型时,只需直接使用func即可。
函数指针用作参数时有两种形式:
int userFunc(int,int,int (int,int));
int userFunc(int,int,int (*)(int,int));
返回指向函数的指针:
int (*ff(int))(int,int);
该定义表示ff是一个函数,有一个int参数,这个函数返回一个指向函数的指针,这个指向函数的指针指向这样一个函数:拥有两个int参数,返回一个int值。
使用typedef可使该定义更易理解:
typedef int(*PF)(int,int);
PF ff(int);
可以把ff(int)想象成一个普通的标识符,如前面例子中的func。
对于类的成员函数,形参表和函数体出现在成员名字之后,处于类作用域中,但返回类型成出现在成员名字之前,如果函数在类定义体之外定义,则用于返回类型的名字在类的作用域之外,如果返回类型使用由类定义的类型,则应使用其完全限定名。
static数据成员必须在类定义体外部定义,在定义时初始化,整型(char,int,short,long及相应无符号的)const static成员可在类定义体内初始化。
将复制构造函数定义为private可防止对象被复制,但类的友元和成员仍可以复制,若想阻止类的友元和成员进行复制,可以声明一个private复制构造函数而不定义。
一般如果需要复制构造函数,也会需要赋值操作符(operator=)。
重载操作符必须具有一个类类型操作数,因为不能对内置类型进行操作符能重载。
重载操作符后操作符的优先级、结合性和操作数数目不能改变。
重载操作符并不保证操作数的求值顺序,尤其是不会保证&&,||, 逗号操作符的操作数求值,在&&和||的重载版本中,两个操作数都要进行求值,而且对操作数的求值顺序不做规定,即其短路求值特性消失,所以重载这三个操作符不是好做法。
既定义了算术操作符又定义了相关复合赋值操作符的类,一般应使用复合赋值操作符实现算术操作符(在算术操作符中简单地调用相关的复合赋值操作符)。
赋值操作符必须定义为类的成员函数,返回对*this的引用。一般而言,赋值操作符与复合赋值操作符应返回左操作数的引用。
类定义下标操作符时,一般需要定义两个版本:一个为非const成员并返回引用,另一个为const成员并返回const引用。
解引用操作符(*)也一般也需要两个版本:const与非const版本。
箭头操作符可能表现得像二元操作符一样:接受一个对象和一个成员名,对对象解引用以获取成员,但箭头操作符不接受显式形参。由编译器处理获取成员的工作。重载箭头操作符时,编译器将按如下对表达式 object->action 进行求值:
1. 如果object是一个指针,指向具有名为action的成员的类对象,则编译器将代码编译为调用该对象的action成员。
2. 否则,如果action是定义了operator->操作符的类的一个对象,则object->action与object.operator->()->action相同,即执行object的operator->(),然后使用返回的结果重复这三步。
3. 否则,代码出错。
函数调用操作符(括号操作符,必须定义为类成员),函数对象:
如下例子,类absInt封闭地int类型的值转换为绝对值的操作。
class absInt
{
int operator() (int val)
{
return val<0?-val:val;
}
}
该类公定义了一个函数调用操作符(括号操作符),通过为该类的对象提供一个实参表而使用调用操作符,所用的方式看起来像一个函数调用:
int i=-23;
absInt abs;
unsigned int ui=abs(i); //cals absInt::operator(int)
尽管abs是一个对象而不是一个函数,但我们仍可以“调用”该对象,效果是运行由abs对象定义的重载调用操作符,该操作符接受一个int值并返回它的绝对值。
定义了调用操作符的类,其对象常称为函数对象,即它们是行为类似函数的对象。
在C++中,基类必须指出希望派生类重定义哪些函数,定义为virtual的函数是基类期待派生类重新定义的,基类希望派生类继承的函数不能定义为虚函数。
除了构造函数外,任意非static成员函数都可以是虚函数,保留字只能在类内部的成员函数声明中出现,不能用在类定义体外部出现的函数定义上。
派生类只能通过派生类对象访问其基类的protected成员,派生类对其基类类型对象的protected成员没有特殊访问权限。
派生类中虚函数的声明必须与基类中的定义方式完全匹配,但有一个例外:返回值为基类型的引用(或指针)的虚函数。派生类中的虚函数可以返回基类函数所返回类型的派生类的引用。比如基类中有一个虚函数返回值为另一个类Base的引用,则此类的派生类中相应的函数可以返回Base类的派生类Derived的引用。
用作基类的类必做是已定义的,只是声明而没有定义的类不能用作基类。因为派生类将包含并须可以访问其基类的成员,为了使用这些成员,派来类必须故道它们是什么。
派生类的前向声明只应包含类名,而不包含派生列表。如class B:public A,则B的前向声明应为:class B,而不是class B:public A。
非虚函数总是在编译时根据调用该函数的对象、引用或指针的类型而确定,称为静态绑定,而虚函数在实际执行的时候根据对象的实际类型调用相应的函数,称为动态绑定。
在某些情况下,可能希望覆盖虚函数机制并强制函数调用使用虚函数的特定版本,这时可以使用域操作符,如:
Base *b=&derived;
b->Base::virtualFunc();
这样做的常见是为了在派生类的虚函数中调用基类中的版本,在这种情况下,基类版本可以完成继承层次中所有类型的公共任务,而每个派生类型只添加自己的特殊工作。
虚函数与默认参数:
虚函数也可以有默认参数,如果一个调用省略了具有默认值的参数,则参数的值由调用该函数的类型定义,与对象的实际类型无关,如果是通过基类的引用或指针调用虚函数,则参数值为在基类虚函数声明中指定的值,如果是通过派生类的引用或指针高用虚函数,则参数值为在派生类虚函数声明中指定的值,也就是说虚函数的默认参数是在编译时静态绑定的,所以应该避免给虚函数声明不同的默认参数,因为用不同的对象调用可能会有不同的行为。
public派生类继承基类的public接口,使用public派生类称为接口继承,使用protected或private派生的类继承基类的成员但不使其成为其接口的一部分,被称为实现继承。
可以通过using改变继承自基类的成员的访问级别,如:
class Base
{
protected:
int i;
};
class Derived:private Base {…};
在这一继承层次中,i 在Base中为protected,但在Derived中为private,为了使i在Derived中为不同的访问级别,可以在Derived相应部分增加一个using声明,如下这样改变Derived的定义,可以使i成员的访问级别恢复为protected:
class Derived : private Base
{
public:
using Base::i;
};
class默认为private继承,struct默认为public继承。
如果基类定义了static成员,则整个继承层次中只有一个这样的成员,无论从基类派生出多少个派生类,每个static成员只有一个实例。如果static成员在基类中为private,则派生类不能访问它。假定可以访问(非private),则既可以通过基类访问,也可以通过派生类访问,既可以使用作用域操作符也可以使用点或箭头成员访问操作符。
对于public继承,派生类对象可以转换为基类对象的引用,不可以转换为基类对象,但可以对基类对象进行初始化和赋值,初始化或赋值时只是将派生类对象的基类部分成员复制到基类对象中,对于非public继承,不可转换为基类对象的引用,不可对基类成员初始化与赋值。
使用dynamic_cast的时候必须有多态即虚函数。
定义派生类的复制构造函数时,一般应显式使用基类复制构造函数初始化对象的基类部分,否则基类部分会通过基类默认构造函数进行初始化。同理,如果派生类定义了赋值操作符,也应调用基类的赋值操作符。(赋值操作符应防止自身赋值)
删除指向动态分配对象的指针时,需要运行释放对象的内存之前清除对象,处理继承层次中的对象时, 指针的静态类型可能与被删除对象的动态类型不同,如果删除基类,则运行基类析构函数并清除基类的成员,如果对象实际是派生类型的,则没有定义该行为,为保证运行适当的析构函数,根类中的析构函数必须为虚函数。所以一般情况下,即使析构函数没有工作要做,继承层次的根类也应该定义一个虚析构函数
像其他虚函数一样,析构函数的虚函数性质会被继承,因此,如果层次中基类的析构函数为虚函数,则派生类的析构函数也将是虚函数,无论派生类是显示定义的析构函数还是合成的。
构造函数不能定义为虚函数,构造函数是在对象完全构造之前运行的,在构造函数运行的时候,对象的动态类型还不完整。可以在基类中将赋值操作符operator= 定义为虚函数,但这样做没有意义,因为赋值操作符有一个形参是自身类类型的引用,不同于任意其他类的赋值操作符的形参类型。
构造派生类对象时首先运行基类的构造函数初始基类部分,在执行基类构造函数时对象的派生类部分是未初始化的,实际上,此时对象还不是一个派生类对象。
撤销派生类对象时,首先撤销它的派生类部分,然后按照与构造顺序的一逆序撤销它的基类部分。
在这两种情况下,对象都是不完整的,为了适应这种不完整,编译器将对象的类型视为在构造或析构期间在发生了变化。在基类的构造函数或析构函数中,将派生类对象当作基类类型对象对待,此时的对象类型对虚函数的绑定会有影响,如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本,因为此时对象不是派生类的类型,派生类的成员没有定义。
名字查找在编译时发生:
每个类都保持着自己的作用域,在该作用域中定义了成员的名字。在继承的情况下,派生类的作用域嵌套在基类作用域中,如果不能在派生类作用域中确定名字,就在外围基类作用域中查找该名字的定义,正是这种类作用域的层次嵌套使我们能够直接访问基类的成员,就好像这些成员是派生类的成员一样。名字的查找在编译时发生,所以对象、引用或指针的静态类型决定了对象能够完成的行为。
继承中的名字冲突:
在派生类中与基类成员同名的派生类成员将屏蔽对基类成员的访问,但可以使用作用域操作符访问被屏蔽的成员。
在基类和派生类中使用同一名字的成员函数,其行为与数据成员一样:在派生类作用域中派生类成员将屏蔽基类成员,即使函数原型不同,基类成员也会被屏蔽,编译器一旦在派生类中找到名字,就不再在基类中查找。
派生类重定义基类函数:
派生类可以重定义从基类继承的函数,如果派生类想通过自身类型使用基类的所有重载版本,则派生类必须要么重定义所有重载版本,要么一个也不定义。
有时类需要仅仅重定义一个重载集合中的某些版本的行为,并且想要继承其他版本的含义,在这种情况下,可以为重载成员提供using声明,一个using声明只能指定一个名字,不能指定形参表,这样可以将基类成员函数的所有重载版本加到派生类的作用域,然后只需重定义相应的版本(using 声明类似为:using Base::func,类限定符加函数名,无需括号与形参表)。
模板函数也可以声明为内联函数,inline说明符应放在模板形参列表之后,返回值之前。
可以使用函数模板对函数指针进行初始化或赋值,这样做的时候,编译器使用指针的类型实例化具有适当模板实参的模板版本。