一. explicit 关键字

explicit 可以将构造函数声明为显式,即禁止隐式转换

什么是隐式转换,它是怎么发生的?

#include <cstdio>
#include <string>

class Person{
private:
    int age;

public:
    Person(int age)
    :age(age){}

public:
    int getAge(void) const{
        return this->age;
    }
};

void show(const Person &person){
    printf("I'm %d years old.\n", person.getAge());
}

int main(){
   Person me = 5;
   show(me);

   show(7);
   return 0;
}

在这个代码中发生了两次隐式转换,这样的隐式转换有时很方便,但有时会带来意想不到的后果

尽可能地使用 explicit, 因为它禁止编译器执行非预期的隐式转换,除非你有更好的理由允许这种隐式转换

二. const 修饰成员函数

const 修饰成员函数,需要放在成员函数的后面。如果放在开始,就是在修饰成员函数的返回值了。

  1. const 修饰成员函数,不能修改成员变量
  2. const 修饰成员函数实际修饰的是该成员函数隐含的 this 指针,即 this 变为了常量指针常量

1. const 对象可以调用非 const 成员函数吗?

不可以,原因在于用 const 所修饰的对象是一个常量,常量中的内容是不可以被修改的,但是普通成员函数是可以修改对象的内容的,二者矛盾,因此无法调用

2. 非 const 对象可以调用 const 成员函数吗?

可以的,因为普通类型的对象既可以修改成员变量的值,也可以选择不去修改

3. const 成员函数内可以调用其它非 const 成员函数吗?

不可以,因为 const 成员函数是只读的,非 const 成员函数可读可写,前后矛盾

4. 非 const 成员函数可以调用 const 成员函数吗?

可以,非 const 成员函数可读可写,const 成员函数只读,可以选择修改也可以选择不修改

三. 拷贝构造函数

1.1 简介

拷贝构造函数通常只有一个参数,指向自身类型的 const 引用

注意不要在拷贝构造函数前加 explicit,没有意义,还会报错

#include <cstdio>
#include <string>

class Person{
private:
    int age;

public:
    explicit Person(int age)
    :age(age){}

    Person(const Person &person)
    :age(person.age){
        printf("I'm copy constructor.\n");
    }

public:
    void show(void) const{
        printf("I'm %d years old.\n", this->age);
    }
};

int main(){
   Person me(5);
   me.show();

   Person you(me);
   you.show();
   return 0;
}

在 linux 下,编译器有时会对构造函数的调用做优化,我们在学习阶段,关闭这个优化

g++ main.cpp -o run -fno-elide-constructors && ./run

如果我们没有为一个类定义拷贝构造函数,那么编译器会为我们定义一个,被称为合成的拷贝构造函数,它只是简单的将参数的成员逐个拷贝到正在创建的对象中。

有时这很方便,但有时却会出现严重的问题,这个我们以后再讲。

1.2 拷贝构造函数什么时候发生调用

拷贝构造函数不仅在我们定义变量的时候会发生,下列情况也会发生

  1. 将一个对象作为实参传递给一个非引用形参
void Test1(Person person){
    person.show();
}
  1. 从一个返回类型为非引用类型的函数返回一个对象,并且类中未定义移动构造函数时
Person Test2(void){
    Person he(7);
    return he;
}

这解释了为什么拷贝构造函数自己的参数为什么必须是引用类型,如果参数不是引用类型,则调用永远不会成功。为了调用拷贝构造函数,我们必须拷贝它的实参,但为了拷贝实参,我们又需要调用拷贝构造函数,如此无限循环

四. 拷贝赋值运算符

类不仅可以控制对象如何初始化,类也可以控制其对象如何赋值

#include <stdio.h>

class Person{
private:
    int age;

public:
    explicit Person(int age)
    :age(age){}

    Person &operator=(const Person &person){
        this->age = person.age;
        printf("I'm copy operator =.\n");
        return *this;
    }

public:
    void show(void) const{
        printf("I'm %d years old.\n", this->age);
    }
};

int main(){
   Person me(5);
   Person you(10);
   you.show();
   you = me;
   you.show();
   return 0;
}

与构造函数一样,如果类未定义自己的拷贝赋值运算符,编译器会为其合成一个。同拷贝构造函数一样,这有时很方便,有时会出现严重的问题。

作业

1. 假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

示例 1:

输入: g = [1,2,3], s = [1,1]

输出: 1

解释:你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。所以你应该输出1。

示例 2:

输入: g =[1,2], s = [1,2,3]

输出: 2

解释:你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。你拥有的饼干数量和尺寸都足以让所有孩子满足。所以你应该输出2.