Skip to content
0

C++面向对象

特殊成员函数

构造函数

  1. 当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数。如果有定义构造函数,又想使用默认构造函数,则必须显式定义默认构造函数。
  2. 默认构造函数只能有一个,而构造函数可以有多个。
  3. 默认构造函数可以没有参数,如果有,则必须给所有参数都提供默认值。

构造函数的初始化方式

  1. 初始化列表
  2. 构造函数体内赋值
  3. 使用默认参数
  4. 委托构造
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;
    }
};

析构函数

  1. 如果没有定义,编译器提供默认析构函数。
c++
// 让编译器自动生成这个析构函数的默认实现
virtual ~Person() = default;
// = default 等价于手动编写空实现的析构函数
virtual ~Person() { }

复制构造函数

复制构造函数用于将一个对象复制到新创建的对象中。其原型如下:

c++
ClassName(const ClassName &);  // 声明

ClassName::ClassName(const ClassName & cls) {}  // 定义

如果没有定义,编译器会提供默认复制构造函数,其行为是逐个拷贝非静态成员变量(成员复制也称为浅复制),复制的是成员的值。

什么情况下会调用复制构造函数?

  1. 新建一个对象并将其初始化为同类型现有对象时。
    c++
    // 假设Person类,已有实例对象p1
    Person p2(p1);
    Person p2 = p1;
    Person p2 = Person(p1);
    Person* ptr = new Person(p1);
  2. 生成对象副本时,如函数按值传递对象或函数返回对象。

赋值运算符

通过重载赋值运算符实现类对象赋值,其原型如下:

c++
ClassName& operator=(const ClassName &); // 声明

ClassName& ClassName::operator=(const ClassName & cls)  // 定义
{
  if (this == &cls)
  {
    return *this;
  }
  ...
  return *this;
}

何时会调用赋值运算符?将已有对象赋给另一个已有对象时。

如果没有定义,编译器会提供默认赋值运算符,其行为同复制构造函数。

在构造函数中使用new时要特别小心

  1. 构造函数中使用new初始化指针成员,则析构函数中应该使用delete。
  2. 如果有多个构造函数,则必须以相同方式使用new,要么都带中括号,要么都不带。因为析构函数只有一个,newdeletenew[]delete[]要对应。
  3. 应该定义一个复制构造函数,通过深拷贝将一个对象初始化为另一个对象。
  4. 应该重载赋值运算符,通过深拷贝将一个对象复制给另一个对象。

封装

访问修饰符JavaC#C++
public都可见
protected同一包内的类本身和派生类可见类本身和派生类可见
private只对类本身可见
缺省同一包内可见类是internal,成员是privateprivate
  • 类的实例对象不属于类本身。
  • C#有5个访问修饰符,可指定7种可访问性级别。
  • C++的访问修饰符只能修饰成员,不能修饰类;而Java和C#的修饰符能修饰类和类的成员。

继承

访问控制

C++类继承时可以指定继承的访问级别。

基类成员/继承方式privateprotectedpublic
private
protectedprivateprotectedprotected
publicprivateprotectedpublic
  • 基类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;
}
  • 对于保护继承和私有继承,基类的公有方法在派生类中属于保护成员和私有成员。如何让基类的公有方法在派生类外也能访问呢?
    1. 在派生类定义一个公有方法调用基类的公有方法。
    2. 在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方法呢?这就会产生歧义性。如何解决?

  1. 在SingingWaiter类中重新定义show方法。(推荐)
  2. 使用作用域解析运算符:singingWaiter.Singer::show();这将调用Singer类的show方法。

多态

同一个方法在不同的派生类中有不同的行为。

实现方式:

  • 重新定义:在派生类中重新定义基类的方法,通过不同的派生类调用方法。
  • 虚函数:
    1. 派生类必须对基类的虚函数进行重写,重写的函数可以继续声明为虚函数。
    2. 必须通过基类的指针(或引用)调用虚函数,但实际给的是派生类的指针(或引用)。

对于方式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 表示纯虚函数
  • 一个类继承抽象类,它必须实现抽象类的所有纯虚函数,才能称为非抽象类。
  • 抽象类可以有构造函数。