点击下载PDF版

C++程序设计基础 第六章.pdf

前言

本文档由 @ItsJiale 创作,作者博客:https://jiale.domcer.com/,作者依据数学与大数据学院 2024 级大数据教学班的授课重点倾向编写而成。所有内容均为手动逐字录入,其中加上了不少自己的理解与思考,耗费近一周时间精心完成。

此文档旨在助力复习 C++ 程序设计基础,为后续学习数据结构筑牢根基。信计专业的同学,也可参考本文档规划复习内容。需注意,若个人学习过程中存在不同侧重点或对重难点的理解有差异,应以教材内容为准。倘若文档内容存在任何不妥之处,恳请各位读者批评指正。

By:ItsJiale

2025.4.8

第6章 类与对象

6.1 面向对象程序设计思想

  1. 继承
  2. 多态
  3. 封装

6.2 认识类和对象

6.2.1 类的定义

类是对象的抽象,是一种自定义数据类型,它用于描述一组对象的共同属性和行为。类的定义格式如下:

class 类名{

权限控制符;

成员;

};

注:

  1. 一定要在{ }后面加上“;”
  2. 权限控制符包括:

public、protected、private

注意:默认不写权限控制符就是“private”

既可以在类内实现成员函数,也可以在类外实现成员函数。在类外实现成员函数的时候,必须在返回值之后、函数名之前加上所属的类作用域即“类名::”,表示函数属于哪个类。例如:

void _**Student ::**_** **study( ){

cout<<"学习C++"<<endl;

}

//加粗斜体的意思是 确认Student类里的成员函数的作用域

Q:为什么要加这个“::”?

A:万一多个类里面都有study这个函数,不加作用域符就无法区分调用是哪个类里的study函数

6.2.2 对象的创建与使用

对象的定义格式如下所示:

类名 对象名;

Student stu;

创建了类的对象stu之后,系统就要为对象分配内存空间,用于存储对象成员

对象的成员变量和成员函数的访问可以通过对象访问符”.“实现,即:
对象名.成员变量

对象名.成员函数

6.3 封装

在设计类时,要控制成员变量的访问权限,不允许外界随意访问。

通过权限控制符可以限制外界对类的成员变量的访问,将对象的状态,将对象的状态信息隐藏在对象内部,通过类提供的函数(接口)实现对类中成员的访问。在定义类时,将类中的成员变量设置为私有或保护属性,即使用private或protected关键字修饰成员变量。使用类提供的公有成员函数(public修饰的成员函数),如用于获取成员变量值的getXXX()函数和用于设置成员变量值的setXXX()函数,操作成员变量的值。

与Java中的getter、setter方法作用类似

6.4 this指针

this 指针就像是每个对象自带的 “身份证”

区分同名变量

当类的成员函数里有和成员变量名字一样的参数时,this 指针能帮你分清哪个是成员变量

#include <iostream>
class Box {
public:
    int length;
    Box(int length) {
        this->length = length;
    }
};

这里构造函数的参数 length 和成员变量 length 重名了,用 this->length 就能准确给成员变量赋值。

返回对象自己

成员函数里能用 this 指针返回对象自身,方便进行连续操作。

#include <iostream>
class Counter {
private:
    int count;
public:
    Counter() : count(0) {}
    Counter& increase() {
        count++;
        return *this;
    }
};

increase 函数返回 *this,也就是对象自己,这样就能连续调用 increase 函数。

简单来说,this 指针能让你准确访问对象的成员变量,还能让对象方便地 “串” 起来操作。

6.5 构造函数

构造函数是类的特殊成员函数,用于初始化对象。构造函数在创建对象时由编译器自动调用。C++中的每一个类至少有一个构造函数,如果类中没有定义构造函数,系统会提供一个默认的无参构造函数,默认的无参构造函数体也为空,不具有实际的初始化意义。

6.5.1 自定义构造函数

class 类名{

权限控制符:

构造函数名(形参列表){

函数体

}

};

关于构造函数定义格式的说明,具体如下:

(1)构造函数名必须要和类名相同

(2)构造函数名的前面不需要设置返回值类型

(3)构造函数中无返回值,不能使用return返回

(4)构造函数的成员权限控制符一般设置为public

自定义无参构造函数

自定义无参构造函数时,可以在函数内部直接给成员变量赋值。

#include <iostream>
using namespace std;

class Person {
    string name;
    int age;
public:
    // 自定义无参构造函数
    Person() {
        name = "无名氏";
        age = 0;
    }

    void introduce() {
        cout << "我叫" << name << ",今年" << age << "岁。" << endl;
    }
};

int main() {
    Person p;
    p.introduce();
    return 0;
}

自定义有参构造函数

#include <iostream>
using namespace std;

class Circle {
    double radius;

public:
    // 自定义有参构造函数
    Circle(double r) {
        radius = r;
    }

    double getArea() {
        return 3.14 * radius * radius;
    }
};

int main() {
    // 创建 Circle 对象并传入半径参数
    Circle c(5.0);
    cout << "圆的面积是: " << c.getArea() << endl;
    return 0;
}

6.5.2 重载构造函数

在C++中,构造函数允许重载。关于函数重载详细请看第五章内容。

6.5.3 含有成员对象的类的构造函数

一个对象作为另一个类的成员变量,即类中的成员变量可以是其他类的对象,这样的成员变量称为类的子对象或成员对象。

class B {

A a; //对象a作为类B的成员变量

......

};

创建含有成员对象的对象时,先执行成员对象的构造函数,再执行类的构造函数。上述例子中,在创建类B对象时,先执行类A的构造函数,将类A对象创建出来,再执行类B的构造函数,创建类B对象。如果类A构造函数有参数,其参数要从类B的构造函数中传入,且必须要以“:”运算符初始化类A对象。

#include <iostream>
using namespace std;

// 类 A
class A {
public:
    A(int num) {
        cout << "A 构造,参数: " << num << endl;
    }
};

// 类 B
class B {
public:
    B(int val) : a(val) {
        cout << "B 构造" << endl;
    }
private:
    A a;
};

int main() {
    B obj(5);
    return 0;
} 
  1. 类 A:有一个带参数的构造函数,当创建 A 的对象时,会输出接收到的参数值。
  2. 类 B:包含一个 A 类型的成员对象 a。B 的构造函数接收一个参数 val,通过初始化列表将 val 传递给 A 的构造函数来初始化 a,之后输出提示信息表明 B 的构造函数已执行。
  3. main 函数:创建 B 的对象 obj 并传入参数 5。程序会先调用 A 的构造函数完成 a 的初始化,再调用 B 的构造函数。
    补充:
  4. 基本概念
    在 C++ 构造函数里,初始化列表位于构造函数参数列表的后面,用冒号 : 开头,接着是一系列用逗号分隔的成员变量初始化项。在上述代码中,B 类的构造函数 B(int val) : a(val) 里,a(val) 就是初始化列表中的一项。
  5. : a(val) 的具体含义
    ● a:指的是 B 类中的成员变量,其类型为 A。
    ● val:是 B 类构造函数的参数。
    ● a(val):表示在 B 类的构造函数体执行之前,调用 A 类的构造函数 A(int num),并把 val 作为参数传递给 A 类的构造函数,以此来初始化 B 类的成员变量 a。

6.6 析构函数

创建对象时,系统会为对象分配所需要的内存空间等资源,当程序结束或对象被释放时,系统为对象分配的资源也需要回收,以便可以重新分配给其他对象使用。

与构造函数一样,析构函数也是类的一个特殊成员函数

class 类名{

~析构函数名称();

......

};

  1. 析构函数的名称与类名相同,在析构函数名称前提前加“~”符号
  2. 析构函数没有参数。因为没有参数,所以析构函数不能被重载,一个类中只能有一个析构函数
  3. 析构函数没有返回值,不能在析构函数名称前添加任何返回值类型。在析构函数内部,也不能通过return返回任何值

当程序结束时,编译器会自动调用析构函数完成对象的清理工作,如果类中没有定义析构函数,编译器会自动提供一个默认的析构函数,但默认的析构函数只能完成栈内存对象的资源清理,无法完成堆内存对象的清理。

析构函数的调用情况

  1. 在一个函数总定义了一个对象,当函数调用结束时,对象应当被释放,对象释放之前编译器会调用析构函数释放资源
  2. 对于static修饰的对象和全局对象,只有在程序结束时编译器才会调用析构函数
  3. 对于new运算符创建的对象,在调用delete释放时,编译器会调用析构函数释放资源

注意:析构函数的调用顺序与构造函数的调用顺序时相反的。在构造对象和析构对象时,遵循原则是:先构造的后析构,后构造的先析构

6.7 拷贝构造函数

6.7.1 拷贝构造函数的定义

拷贝构造函数一种特殊的构造函数,它具有构造函数的所有特性,并且使用本类对象的引用作为形参,能通过一个已经存在的对象初始化该类的另一个对象。拷贝构造函数的参数是同类对象的引用,通常为 <font style="color:rgba(0, 0, 0, 0.85);">const</font> 引用,其作用是将已存在对象的属性复制给新创建的对象。

class 类名{

public:

构造函数名(const 类名& 对象名){

函数体

}

....... //其他成员

};

#include <iostream>
using namespace std;

class MyClass {
public:
    int num;

    // 普通构造函数
    MyClass(int n)  {
        num = n;
    }

    // 拷贝构造函数
    MyClass(const MyClass& other) {
        num = other.num;
    }
};

int main() {
    MyClass obj1(20);
    MyClass obj2(obj1);

    cout << "obj2 的值: " << obj2.num << endl;
    return 0;
}
  1. **MyClass**

    • num 是一个整数类型的成员变量。
    • MyClass(int n) 是普通构造函数,用于初始化 num 成员变量。
    • MyClass(const MyClass& other) 是拷贝构造函数,它接收一个 MyClass 类型对象的常量引用 other,并将 othernum 值赋给新创建对象的 num 成员变量。
  2. **main** 函数

    • 创建 obj1 对象,初始值为 20
    • 使用拷贝构造函数,通过 obj1 创建 obj2 对象。
    • 输出 obj2num 值。

6.7.2 浅拷贝

如果类中有指针类型的数据,默认拷贝构造函数只是简单的指针赋值,即将新对象的指针成员指向原有对象的指针指向的内存空间,并没有为新对象的指针成员申请新空间,这种情况称为浅拷贝。

#include <iostream>
using namespace std;

class SimpleClass {
public:
    int* numPtr;

    // 构造函数
    SimpleClass(int num) {
        numPtr = new int(num);
    }

    // 浅拷贝构造函数
    SimpleClass(const SimpleClass& other) {
        numPtr = other.numPtr;
    }

    // 析构函数
    ~SimpleClass() {
        delete numPtr;
    }

    // 打印指针指向的值
    void printValue() {
        cout << *numPtr << endl;
    }
};

int main() {
    SimpleClass obj1(5);
    SimpleClass obj2(obj1);

    obj1.printValue();
    obj2.printValue();

    return 0;
}
  1. **SimpleClass**

    • int* numPtr:一个指向 int 类型的指针,用于存储动态分配的整数。
    • 构造函数SimpleClass(int num)numPtr 分配内存,并将传入的 num 值存于这块内存中。
    • 浅拷贝构造函数SimpleClass(const SimpleClass& other) 直接把 other 对象的 numPtr 指针赋值给新对象的 numPtr,这就使得两个对象的 numPtr 指向同一块内存。
    • 析构函数~SimpleClass() 释放 numPtr 指向的内存。
    • **printValue** 函数:输出 numPtr 指向的值。
  2. **main** 函数

    • 创建 obj1 对象,初始值设为 5
    • 用浅拷贝构造函数通过 obj1 创建 obj2 对象。
    • 分别输出 obj1obj2numPtr 指向的值。

    ● 注意:
    ● 当程序结束时,obj1 和 obj2 都会调用析构函数,而它们的 numPtr 指向同一块内存,这就会导致这块内存被释放两次,从而引发未定义行为。

6.7.3 深拷贝

解决浅拷贝可能出现的内存问题,可使用深拷贝。深拷贝,就是在拷贝构造函数中完成更深层次的复制,当类中有指针成员时,深拷贝可以为新对象的指针分配一块内存空间,将数据复制到新空间。

#include <iostream>
using namespace std;

class SimpleDeepCopy {
public:
    int* num;

    // 构造函数
    SimpleDeepCopy(int n) {
        num = new int(n);
    }

    // 深拷贝构造函数
    SimpleDeepCopy(const SimpleDeepCopy& other) {
        num = new int(*(other.num));
    }

    // 析构函数
    ~SimpleDeepCopy() {
        delete num;
    }

    // 打印数字
    void print() {
        cout << *num << endl;
    }
};

int main() {
    SimpleDeepCopy obj1(5);
    SimpleDeepCopy obj2(obj1);

    *obj1.num = 10;
    obj1.print();
    obj2.print();

    return 0;
}    
  1. SimpleDeepCopy 类:
    ○ int* num:一个指向整数的指针,用于存储动态分配的整数。
    ○ 构造函数:SimpleDeepCopy(int n) 为 num 分配内存,并将传入的 n 值存储在该内存中。
    ○ 深拷贝构造函数:SimpleDeepCopy(const SimpleDeepCopy& other) 为新对象的 num 分配新的内存空间,并将 other.num 指向的值复制到新的内存中,这样两个对象的 num 指向不同的内存。
    ○ 析构函数:~SimpleDeepCopy() 释放 num 指向的内存。
    ○ print 函数:输出 num 指向的值。
  2. main 函数:
    ○ 创建 obj1 对象,初始值设为 5。
    ○ 使用深拷贝构造函数通过 obj1 创建 obj2 对象。
    ○ 修改 obj1 的 num 指向的值为 10。
    ○ 分别打印 obj1 和 obj2 的 num 指向的值。

核心区别

通过深拷贝,obj1obj2 拥有各自独立的内存空间。所以修改 obj1 的数据不会影响 obj2 的数据,这与浅拷贝形成鲜明对比。

6.8 关键字修饰类的成员

如果只允许类的成员函数读取成员变量的值,但不允许在成员函数内部修改成员变量的值,此时就需要使用const关键字修饰成员函数;或者,类中的成员变量在多个对象之间共享,此时就需要使用static关键字修饰成员变量。

6.8.1 const修饰类的成员

const修饰成员变量

使用const修饰成员变量称为常成员变量。对于常成员变量,仅仅能读取,不能修改。

class MyClass {
public:
    const int value;
    MyClass(int v) {
        value = v;
    }
    void show() {
        cout << "Value: " << value << endl;
    }
};

int main() {
    MyClass obj(25);
    obj.show();
    return 0;
}    

int main() {
    MyClass obj(25);
    obj.value = 30; // 错误:不能修改常成员变量
    return 0;
}

const修饰成员函数

使用const修饰的成员函数成为常成员函数。

返回值 函数名( ) const ;

在常成员函数内部,_只能访问类的成员变量_,而不能修改类的成员变量;并且,常成员函数只能调用类的常成员函数,而不能调用类的非常成员函数,_即:只能读不能写_

#include <iostream>
using namespace std;

class Test {
private:
    int data;
public:
    Test(int d)  {
        data = d;
    }

    // 常成员函数,只能读取成员变量
    int readData() const {
        return data;
    }

    // 非常成员函数,可修改成员变量
    void changeData(int newData) {
        data = newData;
    }
};

int main() {
    Test obj(10);
    const Test constObj(20);

    // 常成员函数读取数据
    cout << "obj 的数据: " << obj.readData() << endl;
    cout << "constObj 的数据: " << constObj.readData() << endl;

    // 修改数据,只能用非常成员函数
    obj.changeData(30);
    cout << "修改后 obj 的数据: " << obj.readData() << endl;

    // 下面代码会报错,const 对象不能调用非常成员函数
    // constObj.changeData(40);

    return 0;
}    

6.8.2 static修饰类的成员

类中的成员变量,在某些时候被多个类的对象共享,实现对象行为的协调作用。共享数据通过static实现,用static修饰成员后,创建的对象都共享一个静态成员且:_延长变量的生命周期_

static修饰成员变量

static修饰的静态成员变量_只能在类的内部定义(公共内存区间),在类外部初始化。_静态成员变量在调用时,可以通过对象和类进行访问。由于static成员变量存储在类的外部,计算类的大小是不包含在内。

#include <iostream>
using namespace std;

class SimpleClass {
public:
    static int count;
    SimpleClass() {
        count++;
    }
};

// 类外初始化静态成员变量
int SimpleClass::count = 0;

int main() {
    SimpleClass obj1;
    SimpleClass obj2;

    // 通过类名访问静态成员变量
    cout << "对象数量: " << SimpleClass::count << endl;
    
    return 0;
}    

static修饰成员函数

使用static修饰的成员函数,同静态成员变量一样,可以通过对象或类调用。

静态成员函数只能访问类中的静态成员变量和静态成员函数。注意:静态成员函数属于类,不属于对象,没有this指针。

#include <iostream>
using namespace std;

class Test {
public:
    static int num;
    static void printNum() {
        cout << "静态成员变量 num 的值: " << num << endl;
    }
};

// 类外初始化静态成员变量
int Test::num = 10;

int main() {
    // 通过类名调用静态成员函数
    Test::printNum();

    Test obj;
    // 通过对象调用静态成员函数
    obj.printNum();

    return 0;
}    
  1. 类的定义:
    ○ static int num;:声明了一个静态成员变量 num,它被所有 Test 类的对象共享。
    ○ static void printNum():定义了一个静态成员函数 printNum,用于输出静态成员变量 num 的值。由于它是静态成员函数,没有 this 指针,只能访问静态成员变量和其他静态成员函数。
  2. 静态成员变量初始化:在类外使用 int Test::num = 10; 对静态成员变量 num 进行初始化。
  3. main 函数:
    ○ Test::printNum();通过类名直接调用静态成员函数 printNum。
    ○ 创建 Test 类的对象 obj,并使用 obj.printNum(); 通过对象调用静态成员函数。

6.9 友元

使用友元可以访问类中的所有成员,函数和类都可以作为友元。

6.9.1 友元函数

友元函数可以是类定义的函数或者是其他类中的成员函数,若在类中声明某一函数为友元函数,则该函数可以操作类中的所有数据。

注意:

友元函数不是类的成员函数

● 成员函数是在类的定义内部声明,并且通常也在类的作用域内实现(可以在类内直接实现,也能在类外通过作用域解析运算符 :: 实现)。成员函数属于类,它能直接访问类的所有成员(包括私有成员和保护成员),并且隐含一个 this 指针,指向调用该函数的对象。
● 友元函数是在类外部定义的普通函数,只是在类的定义里使用 friend 关键字进行了声明。它并不在类的作用域内,也没有隐含的 this 指针。

class MyClass {
private:
    int privateData;
public:
    // 声明友元函数
    friend void friendFunction(MyClass obj);
};

// 友元函数定义在类外部
void friendFunction(MyClass obj) {
    // 能访问类的私有成员,但不是类的成员函数
}

普通函数作为友元函数

将普通函数作为类的友元函数,在类中使用friend关键字声明该普通函数就可以实现,友元函数可以在类中任意位置声明。

class 类名{

friend 函数返回值类型 友元函数名(形参列表);

....... //其他成员

};

友元函数可以访问类中的私有成员,且具有修改私有成员的权限

#include <iostream>
#include <string>
using namespace std;

// 定义 Person 类
class Person {
private:
    string name;
    int age;

public:
    // 构造函数
    Person(string n, int a) {
    name = n;
    age = a;
}
    // 声明友元函数
    friend void printPersonInfo(Person p);
};

// 友元函数的实现
void printPersonInfo(Person p) {
    cout << "Name: " << p.name << ", Age: " << p.age << endl;
}

int main() {
    // 创建 Person 对象
    Person person("Alice", 25);

    // 调用友元函数
    printPersonInfo(person);

    return 0;
}    

Q:为什么类外实现友元函数不需要再加friend?
A:在类的定义里运用 friend 关键字声明一个函数为友元函数,这一操作的目的是赋予该函数访问类私有成员的权限。此声明仅仅是在类的作用域内表明该函数是友元,而在类外部对这个友元函数进行实现时,friend 关键字就没有必要了,因为实现函数时并不需要再次声明它的友元身份。

其他类的成员函数作为友元函数

其他类中的成员函数作为本类的友元函数时,需要在本类中表明该函数的作用域,并添加友元函数所在类的前向声明

class B; //声明类B

class A{

public:

int func( ) ; //声明成员函数func()

};

class B{

friend int A :: func( ); //声明A的成员函数func()为友元函数

};

#include <iostream>
using namespace std;

// 前向声明 OtherClass
class OtherClass;

// 定义 MyClass
class MyClass {
private:
    int num = 100;
    // 声明 OtherClass 的成员函数 display 为友元函数
    friend void OtherClass::display(MyClass obj);
};

// 定义 OtherClass
class OtherClass {
public:
    void display(MyClass obj) {
        cout << "访问 MyClass 的私有成员 num: " << obj.num << endl;
    }
};

int main() {
    MyClass myObj;
    OtherClass otherObj;
    otherObj.display(myObj);
    return 0;
}
 

● 类的声明与定义:
○ 对 OtherClass 进行了前向声明,为 MyClass 里引用 OtherClass 的成员函数做准备。
○ MyClass 类含有私有成员 num,并把 OtherClass 的 display 函数声明为友元函数。
○ OtherClass 类定义了 display 函数,该函数接收一个 MyClass 对象,而非对象的引用。
● main 函数:创建 MyClass 和 OtherClass 的对象,然后调用 OtherClass 的 display 函数,将 MyClass 对象传递给它,以此访问 MyClass 的私有成员。

6.9.2 友元类

除了可以声明函数为类的友元函数,还可以将一个类声明为友元类,友元类可以声明在类中任何位置。声明友元类后,友元类中的所有成员函数都是该类的友元函数,能够访问该类的所有成员。

class B; //类B前向声明

class A{

};

class B{

friend class A; //声明类A是类B的友元类

};

#include <iostream>
using namespace std;

// 前向声明类A
class A;

// 定义类B
class B {
private:
    int secret = 100;
    // 声明类A为友元类
    friend class A;
};

// 定义类A
class A {
public:
    void showBSecret(B b) {
        cout << "B的私有成员值是: " << b.secret << endl;
    }
};

int main() {
    B b;
    A a;
    a.showBSecret(b);
    return 0;
}    

友元函数就像朋友,能够进入你家中的隐私房间,而其他普通人不能

注意:

(1)友元声明位置由程序设计者决定,且不受类中的public、protected、private权限控制符影响

(2)友元关系是单项的

(3)友元关系不具有传递性

(4)友元关系不能被继承

6.10 结构体

结构体是一种用户自定义的数据类型,能把不同类型的数据组合在一起。使用 struct 关键字定义
struct 类名 {
数据特征
};
注意:
使用结构体时需要引入头文件

#include <iostream>
#include<string>
#include<iomanip>   //结构体的头文件
using namespace std;

struct Student
{
    int num;
    string name;
    char sex;
    int age;
    int score;
}; //结构体代表某个类的特征  只有数据成员 没有方法

int main()
{
    struct Student stu1 ={1001,"张三",'M',18,88};
    cout << "num:" << stu1.num << endl;
    cout << "name;" << stu1.name << endl;
    cout << "sex:" << stu1.sex << endl;
    cout << "age:" << stu1.age << endl;
    cout << "socre:" << stu1.score << endl;

    return 0;
}

6.11 联合体

联合体是一种特殊的数据类型,它允许不同类型的变量共享同一块内存空间,通过定义联合体,你可以在同一个内存区域存储不同类型的数据。

同样的,使用联合体也需要引入头文件

下面用例子感受联合体的作用

#include <iostream>
#include<string>
#include<iomanip>   //结构体的头文件
using namespace std;

union Mark { 
//一个变量有多种类型 可以把所有可能的状况列出来 由宏观到具体
    char grade;
    bool pass;
    int percent;

}score;//联合体类型的变量 score

//相当于 union Mark score;

struct Student   
//结构体与类的唯一区别:默认的访问权限是public 类的默认的访问权限是private
{
    int num;
    string name;
    char sex;
    int age;
    union Mark score;  //联合体类型的变量 score
}; //结构体代表某个类的特征 只有数据成员 没有方法

int main()
{
    struct Student stu1 = { 1001,"张三",'M',18,88 };
    cout << "num:" << stu1.num << endl;
    cout << "name;" << stu1.name << endl;
    cout << "sex:" << stu1.sex << endl;
    cout << "age:" << stu1.age << endl;
    cout << "socre:" << stu1.score.percent << endl;  //后缀名发生改变

    return 0;
}
最后修改:2025 年 04 月 12 日
如果觉得我的文章对你有用,请随意赞赏