主题
C++面向对象
特殊成员函数
构造函数
- 当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数。如果有定义构造函数,又想使用默认构造函数,则必须显式定义默认构造函数。
- 默认构造函数只能有一个,而构造函数可以有多个。
- 默认构造函数可以没有参数,如果有,则必须给所有参数都提供默认值。
构造函数的初始化方式
- 初始化列表
- 构造函数体内赋值
- 使用默认参数
- 委托构造
c++
class Person
{
public:
const int id_; // const成员变量只能使用初始化列表的方式
string name_;
short age_;
double height_;
Person(int id, const string& name, short age)
: id_(id), name_(name) { // 1. 初始化列表
age_ = age;
height_ = 0.0; // 2. 构造函数体内赋值
}
Person(int id, const string& name, double height, short age=0) // 3. 使用默认参数
: Person(id, name, age) { // 4. 委托构造
height_ = height;
}
};析构函数
- 如果没有定义,编译器提供默认析构函数。
c++
// 让编译器自动生成这个析构函数的默认实现
virtual ~Person() = default;
// = default 等价于手动编写空实现的析构函数
virtual ~Person() { }复制构造函数
复制构造函数用于将一个对象复制到新创建的对象中。其原型如下:
c++
ClassName(const ClassName &); // 声明
ClassName::ClassName(const ClassName & cls) {} // 定义如果没有定义,编译器会提供默认复制构造函数,其行为是逐个拷贝非静态成员变量(成员复制也称为浅复制),复制的是成员的值。
什么情况下会调用复制构造函数?
- 新建一个对象并将其初始化为同类型现有对象时。c++
// 假设Person类,已有实例对象p1 Person p2(p1); Person p2 = p1; Person p2 = Person(p1); Person* ptr = new Person(p1); - 生成对象副本时,如函数按值传递对象或函数返回对象。
赋值运算符
通过重载赋值运算符实现类对象赋值,其原型如下:
c++
ClassName& operator=(const ClassName &); // 声明
ClassName& ClassName::operator=(const ClassName & cls) // 定义
{
if (this == &cls)
{
return *this;
}
...
return *this;
}何时会调用赋值运算符?将已有对象赋给另一个已有对象时。
如果没有定义,编译器会提供默认赋值运算符,其行为同复制构造函数。
在构造函数中使用new时要特别小心
- 构造函数中使用new初始化指针成员,则析构函数中应该使用delete。
- 如果有多个构造函数,则必须以相同方式使用new,要么都带中括号,要么都不带。因为析构函数只有一个,
new和delete,new[]和delete[]要对应。 - 应该定义一个复制构造函数,通过深拷贝将一个对象初始化为另一个对象。
- 应该重载赋值运算符,通过深拷贝将一个对象复制给另一个对象。
封装
| 访问修饰符 | Java | C# | C++ |
|---|---|---|---|
| public | 都可见 | ||
| protected | 同一包内的类本身和派生类可见 | 类本身和派生类可见 | |
| private | 只对类本身可见 | ||
| 缺省 | 同一包内可见 | 类是internal,成员是private | private |
- 类的实例对象不属于类本身。
- C#有5个访问修饰符,可指定7种可访问性级别。
- C++的访问修饰符只能修饰成员,不能修饰类;而Java和C#的修饰符能修饰类和类的成员。
继承
访问控制
C++类继承时可以指定继承的访问级别。
| 基类成员/继承方式 | private | protected | public |
|---|---|---|---|
| private | ❌ | ❌ | ❌ |
| protected | private | protected | protected |
| public | private | protected | public |
- 基类private成员,不管何种方式继承,派生类都无法访问。
- 基类protected成员,派生类都可以访问。
- private继承:其基类的protected成员在派生类中是private成员。
- protected或public继承:其基类的protected成员在派生类中是protected成员。
- 基类public成员,派生类都可以访问
- private继承:其基类的public成员在派生类中是private成员。
- protected继承:其基类的public成员在派生类中是protected成员。
- public继承:其基类的public成员在派生类中是public成员。
基类方法
派生类不会继承基类的哪些方法:
- 构造函数:派生类需要定义自己的构造函数。创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。派生类通常使用成员初始化列表来调用基类构造函数。
- 析构函数:释放对象时,程序首先调用派生类的析构函数,然后调用基类的析构函数。基类的析构函数应当是虚的。
- 赋值运算符:重载函数的特征标不一样。
- 友元函数:友元函数不属于类成员,因此不能被继承。友元函数是全局的,所以派生类也能直接调用友元函数,但最好是将派生类(指针或引用)强制转换为基类来调用基类的友元函数。
派生类访问基类:
- 访问基类方法:使用作用域解析运算符
::。 - 获取基类对象本身:
cpp
const Person & Student::getPerson() const
{
return (const Person &) *this;
}- 访问基类友元函数:
c++
ostream & operator<<(ostream & os, const Student & s)
{
os << (const Person &) s << " " << s.teacher << endl;
return os;
}- 对于保护继承和私有继承,基类的公有方法在派生类中属于保护成员和私有成员。如何让基类的公有方法在派生类外也能访问呢?
- 在派生类定义一个公有方法调用基类的公有方法。
- 在public中使用
using声明。
c++
class Student : private Person
{
public:
// 注意声明只使用成员名——没有圆括号、函数特征标和返回类型。
using Person::show;
}提示
基类指针(或引用)可以在不进行显式类型转换的情况下指向(或引用)派生类对象,但只能调用基类方法。
多重继承
多重继承带来的问题:SingingWaiter实例对象的Singer对象和Waiter对象都有Worker对象,但实际只需要一个Worker对象。由此C++引入虚基类。
虚基类
虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。
c++
class Singer : public virtual Worker {...};
class Waiter : virtual public Worker {...};
class SingingWaiter : public Singer, public Waiter {...};新的构造函数规则
假设有以下构造函数:
c++
SingingWaiter(const Worker& wk, int s=0, int w=0)
: Singer(wk, s), Waiter(wk, w) {}为避免当wk能通过两条途径传递给Worker对象,C++在基类是虚基类时,禁止信息通过中间类自动传递给基类。上述构造函数将初始化成员s和w,但wk参数中的信息不会传递给子对象Worker,而是使用Worker类的默认构造函数。
如果不希望使用虚基类的默认构造函数,则需显式调用虚基类的构造函数:
c++
SingingWaiter(const Worker& wk, int s=0, int w=0)
: Worker(wk), Singer(wk, s), Waiter(wk, w) {}同名函数
在上面的类图中,假设在SingingWaiter中没有定义show方法,那将调用哪个父类的show方法呢?这就会产生歧义性。如何解决?
- 在SingingWaiter类中重新定义show方法。(推荐)
- 使用作用域解析运算符:
singingWaiter.Singer::show();这将调用Singer类的show方法。
多态
同一个方法在不同的派生类中有不同的行为。
实现方式:
- 重新定义:在派生类中重新定义基类的方法,通过不同的派生类调用方法。
- 虚函数:
- 派生类必须对基类的虚函数进行重写,重写的函数可以继续声明为虚函数。
- 必须通过基类的指针(或引用)调用虚函数,但实际给的是派生类的指针(或引用)。
对于方式1,编译器对非虚函数使用静态联编,在编译阶段根据指针(或引用)的类型,调用类型对应的函数。
对于方式2,编译器对虚函数使用动态联编,在代码执行时根据指针(或引用)所指向的类型(注意不是指针的类型),调用对应的函数。
c++
class Animal
{
public:
virtual void speak(const string& s = "...")
{
cout << "animal speak " << s << endl;
}
}
class Dog : public Animal
{
public:
void speak(const string& s = "wang")
{
cout << "dog speak " << s << endl;
}
}
class Cat : public Animal
{
public:
void speak(const string& s="miao")
{
cout << "cat speak " << s << endl;
}
}c++
void test1()
{
Dog* dog = new Dog();
dog->speak();
Cat* cat = new Cat();
cat->speak();
}c++
void speak(const Animal& animal)
{
animal.speak();
}
void test2()
{
Animal* animal = new Dog();
// 通过基类调用虚函数,实际调用的是派生类的虚函数;
// 但虚函数的默认参数使用的是基类的默认参数
animal->speak();
// 更常见的方式
Dog* dog = new Dog();
Cat* cat = new Cat();
speak(*dog);
speak(*cat);
}虚函数
- 默认参数是静态绑定的(编译阶段已经确定),而虚函数是动态绑定的。所以虚函数的默认参数取决于指针或引用的类型,而不是它们所指向的对象的类型。
- 给基类提供一个虚析构函数准没错。
- 构造函数、静态函数不能是虚函数。
- 友元不能是虚函数,因为友元不是类成员。
抽象类
C++使用纯虚函数提供未实现的函数。包含有纯虚函数的类即为抽象类,不能创建抽象类的实例对象。
c++
virtual void speak() const = 0; // =0 表示纯虚函数- 一个类继承抽象类,它必须实现抽象类的所有纯虚函数,才能称为非抽象类。
- 抽象类可以有构造函数。