跳转至

类和对象

1. 类和对象的基本概念

在 C++ 中, 类是一种由用户定义的数据类型,用于封装数据和方法。

类定义了对象的属性和行为,对象是类的实例,它可以使用类定义的属性和方法。通过类和对象,可以实现面向对象编程的概念,包括如下三大特性。

  • 封装
  • 继承
  • 多态

面向对象则是类的实例化,通过类和对象,可以实现数据的抽象和封装,提高代码的可维护性和可重用性。

在对象上有其属性行为

例如 人就可作为对象,其拥有的属性为姓名、年龄、身高等,其拥有的行为有:走、跑、跳等,具有相同性质的对象即可抽象成为一个类。

2. 封装

2.1 封装的意义

封装将属性和行为作为一个整体,表现一个事物,并将属性和行为加以权限控制。在设计类的时候,属性和行为写在一起,表现事物。

class 类名 { 访问权限: 属性/行为};
求圆周长例
// 圆周率 常量
const double PI = 3.14;

// Circle 类
class Circle {
    /*
     * public 访问权限 - 公共
    */
    public:
    // 圆的属性
    int circle_r;
    // 圆 类的行为
    double calculate(){
        return 2 * PI * circle_r_r;
    }
};

int main(){
    // 通过 Circle 类实例化一个 c1 对象
    Circle c1;
    // 对其属性进行赋值
    c1.circle_r = 10;
    // 通过调用 c1 中的 calculate 行为获取该圆的周长
    cout << "CalcuLate = " << c1.calculate() << endl;
}

2.2 访问权限

类在设计时,可以把属性和行为放在不同的权限下,加以控制。

类的访问权限有三种

  • public — [公共权限]
  • protected — [保护权限]
  • private — [私有权限]

其中不同类型的权限可访问的范围也不同:

权限类型 类内 类外
public 可以访问 可以访问
protected 可以访问 不可访问
private 可以访问 不可访问

protectedprivate 虽然类内类外访问权限相同,但是涉及到继承时存在不同;protected 作为父类被子类继承的时候子类可以访问父类中 protected 的属性、行为。而private不同的是子类无法访问父类中private的属性、行为。

示例
class Person {
// 公共权限
public:
    string name;
// 保护权限
protected:
    string car;
// 私有权限
private:
    int password;
};

2.3 classstruct的区别

C++ 中 classstruct 都可以定义自定义的数据类型,但是他们之间有一定的区别。

2.3.1 默认访问权限

class 中,默认的成员访问权限是 private,而在 struct 中默认的成员访问权限是 public

2.3.2 使用习惯

一般来说 class 更适合具有复杂行为和数据封装的情况,而 struct 更适合用于简单的数据集合。

2.3.3 继承

当使用 class 时,派生类默认继承的访问权限是 private 而当使用 struct 时,默认继承的访问权限是 public

示例
// 默认私有权限
class C1 {
    int m_A;
};

// 默认公共权限
struct S1 {
    int m_A;
};

int main(){
    C1 c1;
    S1 s1;

    // 由于C1默认私有权限,外部无法访问
    c1.m_A = 10;
    // S1默认为公共权限,外部可以访问
    s1.m_A = 10;
}

2.4 成员属性设置为私有

设置成员属性为私有,可以由自己控制读写权限,对于读写权限我们可以检测数据的有效性。

通过提供修改私有数据的接口,可以防止用户直接操作类内的成员,并且通过使用提供的数据修改方法,可以方便检测数据的合法性、有效性等。

示例
class Person {
private:
    // 姓名 读写状态: 可读写
    string name;
    // 年龄 读写状态: 只读
    int age = 10;
    // 爱好 读写状态: 只写
    string hobby;
public:
    void setName (string m_name){
        name = m_name;
    }
    string getName (void){
        return name;
    }
    int getAge (void){
        return age;
    }
    void setHobby (string m_hobby){
        hobby = m_hobby;
    }
};

int main(){

    Person person;
    string temp_name, temp_hobby;

    cout << "设置姓名: ";
    cin >> temp_name;
    person.setName(temp_name);


    cout << "姓名:" << person.getName() << endl;
    cout << "年龄:" << person.getAge() << endl;

    cout << "设置爱好:";
    cin >> temp_hobby;
    person.setHobby(temp_hobby);
}

2.5 构造函数与析构函数

生活中购买使用的电子产品通常具有出场设置, 并且在某一天我们不再使用时也会删除一些信息以确保安全。

在 C++ 中对象的初始化和清理也是两个非常重要的安全问题, 若一个对象或者变量没有初始状态, 则对其使用的后果未知; 同样如果使用完一个对象或变量后不及时进行清理, 也会造成一定的安全问题。

C++ 中使用构造函数析构函数解决上述问题, 这两个函数会被编译器自动调用, 完成对象初始化和清理工作。

此外对象的初始化和清理工作时编译器强制要求的事情, 如果我们不提供构造函数和析构函数, 编译器会提供构造函数和析构函数是空实现。

  • 构造函数: 在创建对象时为对象的成员进行赋值, 由编译器自动调用, 无需手动调用。
  • 析构函数: 在对象销毁前自动调用, 执行一些清理工作。

2.5.1 构造函数

语法

<class_name> () {}
  • 构造函数无返回值, 也不写 void
  • 函数名称与类名相同
  • 构造函数可以有参数, 因此可以发生重载
  • 程序在调用对象时会自动执行构造函数, 无需手动调用并且仅执行一次

2.5.2 析构函数

语法

~<class_name> () {}
  • 析构函数, 没有返回值, 也不写 void
  • 函数名称与类名相同, 在名称前加 ~
  • 析构函数不可以有参数, 因此不可以发生重载
  • 程序在对象销毁前会自动调用析构, 无需手动调用并且仅调用一次

示例

class person{
public:
// 构造函数 - 无参
person(){
    cout << "no parameter structure function" << endl;
}

// 析构函数 - 无参
~person(){
    cout << "no parameter destructors function" << endl;
}
};

int main() {
    person p1;

    system("pause");
    return 0;
}

2.6 构造函数的分类及调用

构造函数根据分类有两种分类:

  • 按参数分类:有参构造和无参构造
  • 按类型分类:普通构造和拷贝构造

构造函数有三种调用方式:

  • 括号法
  • 显示法
  • 隐式转换法

2.6.1 有参构造和无参构造

二者构造方式不同的是,有参构造函数在调用时需要传入参数而无参构造不需要。

如果用户在编写类的时候不写构造函数,则编译器会默认生成一个无参构造函数,因此无参构造函数也称默认构造函数。

无参构造函数
1
2
3
4
5
6
7
8
9
class Person{
public:
    // 无参构造函数
    Person(){
        /*
        需执行的代码
        */
    }
}
有参构造函数
1
2
3
4
5
6
7
8
9
class Person{
public:
    // 无参构造函数
    Person(int age){
        /*
        需执行的代码
        */
    }
}

2.6.2 拷贝构造

拷贝构造函数可以将传入的对象的属性拷贝至当前对象中。

其中对于传入的对象:

  • 需要拷贝的对象需是 const 静态的
  • 通过地址传入需要的对象
1
2
3
4
5
6
7
class Person{
public:
    // 拷贝构造函数
    Person(const Person &p){
        this->age = p.age;
    }
}

2.6.3 括号法调用构造函数

在实例化对象时,默认调用的就是无参构造函数。

Person p1;

在对象名后通过括号填写参数,即为括号法调用有参构造函数。

Person p2(/*age*/ 10);

2.6.4 显示法调用构造函数

Person p3 = Person(/*age*/ 10);

其中 Person(/*age*/ 10) 是一个匿名函数,其在执行完毕后便会马上被回收掉。

拷贝构造函数不能用于初始化匿名对象,编译器会认为 Person (p3) == Person p3

2.6.5 隐式转换法调用构造函数

Person p4 = 10;

这个写法等价于下面的代码

Person p4 = Person(/*age*/ 10);

2.7 拷贝构造函数调用时机

C++ 中拷贝构造函数的调用时机通常来说有三种情况:

  • 使用一个已经创建完毕的对象来初始化一个新的对象。
person p_1(20);
person p_2(p_1);

person 类 p_1 使用了有参构造函数实例化了一个对象,此时将对象 p_1 作为实例化 p_2 对象的参数,此时就调用了 person 类中的拷贝构造函数。

  • 值传递的方式给函数参数传值。
void do_work(person p){};

void int main(){
    person p;
    do_work(p);
}

这里将对象 p 作为了函数 do_work 的形参传入,此时便调用了拷贝构造函数创建了一份副本传入函数 do_work。

  • 以值的方式返回局部对象。
person do_work() {
    person p_temp;

    /* code... */

    return p_temp;
}

在函数 do_work 中创建了一个临时对象 p_temp 在函数执行完毕后返回局部对象的时候,调用了拷贝构造函数并将其返回作为函数的返回值,原对象执行完毕后便销毁。

2.8 构造函数的调用规则

默认情况下,C++ 编译器至少会为一个类添加三个函数:

  • 默认构造函数 (无参构造,函数体为空)
  • 默认析构函数 (无参构造,函数体为空)
  • 默认拷贝构造函数,对属性值进行拷贝

构造函数的调用规则如下:

  • 如果用户定义有参构造函数,C++ 不再默认提供无参构造,但是会默认提供拷贝构造。
  • 如果用于定义拷贝构造函数,C++ 不再提供其它构造函数。

2.9 深拷贝与浅拷贝

  • 深拷贝:在堆区重新申请内存空间,进行拷贝操作。
  • 浅拷贝:简单的复制拷贝操作。
// 浅拷贝堆区内存重复释放
class person{
public:
    person (int t){
        this->t = new int(t);
    }

    ~person (){
        if (this->t != nullptr){
            delete this->t;
            t = nullptr;
        }
    }

    int *t;
}
person p_1(10);
person p_2(p_1);

由于堆的特性,p_2 先执行析构函数释放空间,但是到 p_1 执行析构函数释放空间的时候空间已经被释放,此时便是非法操作。

手动实现深拷贝操作
person (const person &p){
    this->t = new int (*p.t);
}

如果类中的属性有在堆区开辟的,一定要自己提供深拷贝方法,避免浅拷贝带来的问题。

3. 继承

4. 多态