C++面向对象程序设计
- 在C++语言中,当我们使用基类的引用或指针调用一个虚函数时将发生动态绑定,即JAVA中的多态
- 基类通常要定义一个虚析构函数,即使该函数不执行任何实际操作,也是如此。
- 在C++中基类的成员函数有两种:1、基类希望派生类进行重写的函数;2、基类希望派生类直接继承而不用改变的函数。对于前者,基类将其定义为虚函数。当指针或引用调用虚函数时,该调用将被动态绑定(即多态)。
- 任何构造函数之外的非static函数都可以是虚函数。关键字virtual只能出现在类内部的函数声明语句之前,而不能出现在类外部的函数定义语句之前。如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数。
- 如果成员函数没有被声明成虚函数,则其解析过程发生在编译时而非运行时。所以在继承层次中,该函数只有一个,不能被重写成多个版本。
- 派生类必须重新声明基类中所有的虚函数。派生类可以在这样的函数前加上关键字virtual,但并非非得这样做。因为一旦某个函数被声明成virtual,则在所有派生类中它都是虚函数。
- 如果派生列表中基类的访问控制符为public,我们可以将派生类型的对象绑定到基类的引用或指针上。
- 派生类虽然有基类的数据成员,但是也只能通过基类的构造函数来初始化它们。
class A {
public:
int a;
A(int p):a(p) {}
}
class B : public A {
public:
int b;
B(int p1, int p2):A(p1), b(p2) {}
}
//B的构造函数先由A的构造函数初始化a,然后执行A的构造函数体,接着初始化b,最后执行B的构造函数体
//派生类声明时,声明中包含类名但是不包含它的派生列表
class B : public A;//错误
class B;//正确
- 如果某个类被当作基类,则该类必须被定义,而不能仅仅被声明。
class A; //仅仅声明了A类
class B : public A {
...
} //错误,A类必须被定义
- 从派生类向基类的类型转换只对指针或引用类型有效
- 基类向派生类不存在隐式的类型转换
- 当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中基类的部分会被拷贝、移动、赋值,它的派生类部分将被忽略掉。所以得到的基类对象无法强制转换成派生类对象。
class A {}
class B : public A {}
B b;
A a(b);//实际上,调用的是拷贝构造函数,即A::A(const A&)
a = b;//实际上,调用的是拷贝赋值运算符,即A::operator=(const A&)
- 普通函数如果我们不会用到它,可以只声明不定义。但是虚函数必须有定义,不论我们是否使用了它。
- 只有虚函数才可以被覆盖,若类的某个虚函数的定义用final修饰,则该类的派生类就不能再覆盖此虚函数了。
class A {
public:
virtual void f1() const;
void f2() {
cout << "A's f2 is running." << endl;
}
virtual void f3() {
cout << "A's f3 is running." << endl;
}
}
class B : public A {
public:
void f1() const final;//B类覆盖了A中的f1函数,且不允许自身的派生类再覆盖
void f2() {
//B中的f2,并没有覆盖A中的f2。
cout << "B's f2 is running." << endl;
}
void f3() {
//B中的f3,覆盖了A中的f3。
cout << "B's f3 is running." << endl;
}
}
class C : public B {
public:
void f1();//错误,B已经将f1声明成final了
}
int main() {
A a;
B b;
A *pointA = &b;
a.f2();
b.f2();
cout << "pointA->f2:";
pointA->f2();
a.f3();
b.f3();
cout << "pointA->f3: ";
pointA->f3();
cout << "pointA = &a, pointA->f3: ";
pointA = &a;
pointA->f3();
}
//输出结果如下:
/**
A's f2 is running.
B's f2 is running.
pointA->f2:A's f2 is running.
A's f3 is running.
B's f3 is running.
pointA->f3: B's f3 is running.
pointA = &a, pointA->f3: A's f3 is running.
*/
- 虚函数也可以拥有默认实参,如果通过基类的引用或指针调用函数,使用默认实参时,传入派生类函数的将是基类函数定义的默认实参。所以如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致。
- 如果一个派生类的虚函数需要调用它的基类版本,必须使用作用域运算符,否则该调用将被解析成对派生类版本自身的调用。
- 纯虚函数的声明是在虚函数声明语句的分号前加上“=0”,另外我们可以为纯虚函数提供定义,但函数体必须在类外部。
class A {
public:
virtual void doSomeThing();
}
class B : public A {
public:
void doSomeThing() = 0;//将doSomeThing声明为纯虚函数
}
//纯虚函数的函数体定义在类的外部
void B::doSomeThing() {
cout<<"B is doing some thing"<<endl;
}
- 含有未提供定义的纯虚函数的类是抽象基类。我们不能创建抽象基类的对象。派生类必须自己给出这些纯虚函数的定义,否则它们仍然是抽象基类。
- 派生类的成员和友元可以访问派生类对象中由基类继承而来的protected成员,而无法访问一个基类对象中的protected成员。
class A {
protected:
int i;
}
class B : public A {
public:
friend void visit(A&);//不能访问A::i
friend void visit(B&);//能访问B::i
}
void visit(A& a) {
cout << a.i << endl;//错误,不能访问A对象的i
}
void visit(B& b) {
cout << b.i << endl;//正确,能访问B对象的i
}
- 派生类的内部或友元对继承而来的基类成员的访问权限是由基类对成员的访问控制符决定的;派生类的外部(如该派生类的派生类或其它地方)对继承而来的基类成员的访问权限是由派生列表的访问控制符决定的。
- 派生类向基类的类型转换
- 友元不能继承
class BaseA {
friend class BaseB;
protected:
int prot_data;
}
class A : public BaseA {
private:
int data;
}
class BaseB {
public:
virtual int f(BaseA baseA) {
return baseA.prot_data;//正确,BaseB是BaseA的友元
}
int f2(A a) {
return a.data;//错误,BaseB不是A的友元
}
int f3(A a) {
return a.prot_data;//正确,BaseB是A中基类部分的友元
}
}
class B : public BaseB {
public:
int f(BaseA baseA) {
return baseA.prot_data + 1;//错误,友元不能继承,所以B不是BaseA的友元
}
}
- 名字查找先于类型检查。如果派生类的成员与基类的某个成员同名,则派生类将在其作用域内隐藏该基类成员。即使派生类成员和基类成员的形参列表不一致,也仍然会被隐藏掉。
struct Base {
int memfcn();
};
struct Dervied : Base {
int memfcn(int); //隐藏基类的memfcn
};
Dervied d;
Base b;
b.memfcn();//调用Base::memfcn
d.memfcn(10);//调用Derived::memfcn
d.memfcn();//错误:参数列表为空的memfcn被隐藏了。因为Derived定义了一个名为memfcn的成员,编译器找到这个名字后,就不再继续查找了。
d.Base::memfcn();//正确,调用Base::memfcn
- 派生类的函数覆盖基类的函数,必须同时满足以下两点:
1、基类的函数是虚函数
2、派生类的函数名以及形参列表必须和基类的完全一样,派生类的返回类型也必须和基类函数匹配
缺少任意一条,派生类的同名函数都会隐藏基类的函数
派生类的函数是否覆盖基类函数,与该基类函数的访问控制符无关。
用using导入基类中指定的重载函数时,必须保证所有的重载函数对派生类都是可访问的。
class A {
private:
virtual void f();
}
class B : public A {
//using A::f; 错误,f是private,B不可以访问
private:
void f();//照样会覆盖A::f()
}
- 如果一个类需要定义析构函数,则也必须为它定义拷贝构造函数和拷贝赋值运算符。但是基类的析构函数并不遵循上述规则,因为一个基类总是需要析构函数,而且要将它设置成虚函数,此时为了定义这样的一个析构函数而将函数体设置成空。所以无法凭此判断该基类还需要赋值运算符或拷贝运算符。
派生类中删除的拷贝控制与基类的关系
1、如果基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符或析构函数是被删除的函数或者不可访问,则派生类中对应的成员将是被删除的,原因是编译器不能使用基类成员来执行派生类对象基类部分的构造、赋值或销毁操作。
2、如果在基类中有一个不可访问或删除掉的析构函数,则派生类中合成的默认和拷贝构造函数将是被删除的,因为编译器无法销毁派生类对象的基类部分。当为派生类自定义拷贝或移动构造函数时,默认情况下,是由基类默认构造函数初始化派生类对象的基类部分。如果我们想拷贝(或移动)基类部分,则必须在派生类的构造函数初始值列表中显式地使用基类的拷贝(或移动)构造函数。
class Base {
/** ...*/
}
class D : public Base {
//拷贝基类成员
D(const D &d):Base(d) {
/**...*/
}
//移动基类成员
D(D&& d):Base(std::move(d)) {
/**...*/
}
}
//如果按照如下定义,则拷贝出来的对象会很奇怪:它的Base成员被赋予了默认值,而D成员的值则是从其它对象拷贝得来的。
D(const D &d) {
/**...*/
}
- 派生类的合成拷贝构造函数会使用基类的拷贝构造函数。
- 派生类自定义的赋值运算符必须显式调用基类的赋值运算符才能为其基类部分赋值。
//Base::operator=(const Base&)不会自动调用
D & D::operator=(const D &rhs) {
Base::operator=(rhs);//为基类部分赋值
/**...*/
return *this;
}
- 派生类的构造函数(含拷贝、移动构造函数)、赋值运算符、析构函数的合成版会自动调用基类中对应的操作。至于基类的这些操作是合成版还是自定义的都无关紧要,只要它们可以访问,并且不是被删除的即可。
- 派生类自定义的构造函数(含拷贝、移动构造函数)会自动调用基类的无参构造函数,自定义的析构函数会自动调用基类的析构函数。
- 派生类自定义的拷贝构造函数、移动构造函数、赋值运算符,不会自动调用基类的拷贝构造函数、移动构造函数、赋值运算符。
- 对象销毁的顺序正好与其创建的顺序相反:派生类析构函数首先执行,然后是基类的析构函数,以此类推,沿着继承体系的反方向直至最后。
- 如果构造函数或析构函数调用了某个虚函数,则执行的是构造函数与析构函数所在类型的虚函数版本。若构造函数与析构函数是间接调用的虚函数的话(即构造函数或析构函数调用A函数,A函数又调用了B函数,而B函数是虚函数),也遵循上述规定。
- 调用upper_bound函数可以跳过multimap和multiset中关键字相同的元素,例子见《C++ primer》的15.8.1中“定义Baseket的成员”
- 类的构造函数初始化步骤:
1、若有初始化列表,按初始化列表初始化
2、若无初始化列表,但在类内声明时就赋予了初始值,则按该初始值初始化
3、若无初始化列表也无声明时的初始值,则类类型按无参构造函数初始化,基本类型的数据的无定义
class A {
public:
A() {
cout << "the A's creater without parameter is used." << endl;
}
}
class B {
public:
int i = 1;
int k;
A a;
B(){}
B(int j):i(j){}
}
int main() {
B b, b2(2);
cout << "b.i = " << b.i << endl;
cout << "b2.i = " << b2.i << endl;
cout << "b.k = " << b.k << endl;
}
/**
输出结果:
the A's creater without parameter is used.
the A's creater without parameter is used.
b.i = 1
b2.i = 2
b.k = 15315 //k的值未定义
*/
- 列表内容