C++面向对象基础

169 阅读12分钟

面向对象是一种编程范式,它以对象为核心,将数据和操作数据的方法封装在一起,通过抽象、继承、多态等机制来组织代码。面向对象的主要目标是提高代码的可复用性、可维护性和可扩展性。

学习目标

  1. 理解面向对象三大特性
  2. 类与对象
  • 理解类是创建对象的模板,对象是类的实例。
  • 掌握如何定义类,包括成员变量(属性)和成员函数(方法)。
  • 理解构造函数和析构函数的作用,并知道如何自定义它们。
  1. 封装
  • 理解封装的意义,即将数据(属性)和操作数据的方法(函数)封装在一起。
  • 掌握如何通过访问修饰符(public, protected, private)来控制成员的可访问性。
  1. 继承
  • 理解继承的概念,即子类(派生类)继承父类(基类)的属性和方法。
  • 掌握继承的三种类型:公有继承、保护继承和私有继承。
  • 理解并能够实现虚函数和多态。
  1. 多态
  • 理解多态的概念,即同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。
  • 掌握通过虚函数实现运行时多态的方法。
  1. 抽象类与接口
  • 理解抽象类的概念,即包含至少一个纯虚函数的类。
  • 了解接口的概念,接口是一种特殊的抽象类,只包含纯虚函数。

1. 面向对象

面向对象三要素:继承、封装、多态。

1.1. 核心概念

1.1.1. 对象

对象是面向对象编程的基本单元,它是一个具体的实例,包含数据(属性)和行为(方法)。例如,在一个学生管理系统中,每个学生可以看作一个对象,具有姓名、年龄、学号等属性,以及选课、考试等方法。

1.1.2. 类

类是对象的模板或蓝图,它定义了对象的属性和方法。类是抽象的,而对象是类的具体实例。例如,Student类可以定义学生的共同属性和行为,而具体的某个学生(张三)是Student类的一个对象。

1.1.3. 封装

封装是将数据(属性)和操作数据的方法(行为)绑定在一起,并隐藏内部实现细节。通过访问修饰符(如publicprivateprotected),可以控制数据的访问权限,提高代码的安全性和可维护性。

1.1.4. 继承

继承是一种机制,允许一个类(派生类)基于另一个类(基类)创建新的类。派生类继承了基类的属性和方法,并可以扩展或修改这些属性和方法。继承的主要目的是实现代码的复用。

1.1.5. 多态

多态是指同一个接口可以表现出不同的行为。通过多态,可以在运行时决定调用哪个类的具体实现。一般通过虚函数和抽象类来实现多态。

1.2. 优势

  • 模块化:将代码划分为独立的类和对象,便于管理和维护。
  • 可复用性:通过继承和组合,可以复用已有的代码,减少重复劳动。
  • 可扩展性:通过多态和抽象类,可以轻松扩展程序的功能。
  • 可维护性:封装可以隐藏内部实现细节,降低代码耦合性,方便后续修改和调试。

1.3. 代码示例

#include <iostream>
#include <string>

// 定义一个类
class Student {
private:
    // 私有属性
    std::string name;
    int age;

public:
    // 构造函数
    Student(const std::string &name, int age) 
    : name(name), age(age){}
    // 公有方法
    void displayInfo() {
        std::cout << "Name: "<< name << ", Age:" << age << std::endl;
    }
    // 设置年龄
    void setAge(int age) {
        this->age = age;
    }
    // 获取年龄
    int getAge() {
        return age;
    }
};

int main() {
    // 创建对象
    Student student1("Alice", 20);
    Student student2("Bob", 22);
    
    // 调用对象的方法
    student1.displayInfo();
    student2.displatInfo();
    
    // 修改对象的属性
    student1.setAge(12);
    std::cout << "Alice's new age: "<< studnet1.getAge() << std::endl;
    
    return 0;
}

2. 类的继承与派生

2.1. 继承的基本概念

继承是面向对象编程的重要特性之一,它允许一个类(派生类)基于另一个类(基类)来创建新的类。派生类继承了基类的属性和方法,同时可以扩展或修改这些属性和方法。

  • 基类(父类) :被继承的类。
  • 派生类(子类) :继承基类的类。

2.2. 继承的语法

继承的语法如下:

class 派生类名 : 访问修饰符 基类名, 访问修饰符 基类名... {
// 派生类的成员
}

访问修饰符可以是publicprotectedprivate,决定了基类成员在派生类中的访问权限。

如果不带访问修饰符,struct默认public继承,class默认private继承

2.3. 继承的类型

  • 公有继承( public :基类的public成员在派生类中仍然是publicprotected成员仍然是protected
  • 保护继承( protected :基类的publicprotected成员在派生类中都变为protected
  • 私有继承( private :基类的publicprotected成员在派生类中都变为private
#include <iostream>

// 基类
class Animal {
public:
void eat() {
    std::cout << "Animal is eating." << std::endl;
}
protected:
void sleep() {
    std::cout << "Animal is sleeping." << std::endl;
}
};

// 派生类
class Dog : public Animal {
public:
void bark() {
    std::cout << "Dog is barking." << std::endl;
    sleep();
}
};

int main() {
    Dog dog;
    dog.eat(); // 调用基类的public成员函数
    dog.bark(); // 调用派生类的成员函数
    // dog.sleep(); // 错误:sleep是protected成员,不能在类外访问
    return 0;
}

3. 多态

3.1. 多态的概念

多态是指同一个接口可以表现出不同的行为。在C++中,多态主要通过虚函数和抽象类来实现。

3.2. 虚函数

虚函数是在基类中使用virtual关键字声明的函数,可以在派生类中重写该函数。通过基类指针或引用调用虚函数时,实际调用的是派生类的函数。

注意事项:

  1. 子类重写父类虚函数的时候可以不用写override,但使用 override 有好处:
    1. 明确意图:override 关键字能清晰表明该函数是对父类虚函数的重写,增强了代码的可读性。
    2. 编译时检查:若使用 override 标记的函数没有正确重写父类的虚函数,编译器会报错。例如,若不小心改变了函数名、参数列表或者返回类型,编译器就能及时发现问题。
  1. virtual 关键字可选:子类在重写父类的虚函数时,不必重复写 virtual 关键字,但写上也合法(属于冗余但无害的写法)。
  2. 虚函数会一直保持虚特性:只要某个函数在基类中被声明为虚函数,所有派生类中重写的版本都将保持虚特性,无论中间子类是否显式写 virtual
#include <iostream>

class Animal {
public:
virtual void makeSound() {
    std::cout << "Animal makes a sound." << std::endl;
}
};

class Dog : public Animal {
public:
void makeSound() override {
    std::cout << "Dog barks." << std::endl;
}
};

class Cat : public Animal {
public:
void makeSound() override{
    std::cout << "Cat meows." << std::endl;
}
};

int main() {
    Animals* animal1 = new Dog();
    Animals* animal2 = new Cat();

    animal1->makeSound();
    animal2->makeSound();

    delete animal1;
    delete animal2;
    return 0;
}

3.3. 抽象类

抽象类是不能实例化的类,通常用于定义接口。抽象类中至少包含一个纯虚函数(用= 0声明)。

接口:只含有纯虚函数的抽象类。

#include <iostream>

class Shape {
public:
virtual void draw() = 0;
};

class Circle : public Shape {
public:
void draw() override {
    std::cout << "Drawing a Circle." << std::endl;
}
};

class Square: public Shape {
public:
void draw() override {
    std::cout << "Drawing a Square." << std::endl;
}
};

int main() {
    shape* shape1 = new Circle();
    shape* shape2 = new Square();

    shape1->draw();
    shape2->draw();

    delete shape1;
    delete shape2;

    return 0;
}

4. 静态成员与友元函数

4.1. 静态成员

静态成员属于类本身,而不是类的某个对象。静态成员变量在类的所有对象之间共享,静态成员函数只能访问静态成员变量。

静态成员需要在类内声明,类外初始化。

#include <iostream>

class Counter {
public:
static int count; // 静态成员变量
Counter() {
    count++;
}
static void showCount() { // 静态成员函数
    std::cout << "Count: " << count << std::endl;
}
};

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

int main() {
    Counter c1, c2, c3;
    Counter::showCount(); // 输出:Count: 3
    return 0;
}

4.2. 友元函数

友元函数可以访问类的私有成员和保护成员。友元函数不是类的成员函数,但需要在类中声明。

#include <iostream>

class Box {
private:
int width;
public:
Box(int w) : width(w) {}
friend void printWidth(Box box); // 声明友元函数
};

void printWidth(Box box) {
    std::cout << "Width: " << box.width << std::endl; // 访问私有成员
}

int main() {
    Box box(10);
    printWidth(box); // 输出:Width: 10
    return 0;
}

5. this指针与成员函数重载

5.1. this指针

this指针是一个隐含的指针,指向当前对象的地址。它用于在类的成员函数中访问当前对象的成员。

类的非静态成员函数都有一根this指针指向调用函数的对象。

#include <iostream>

class MyClass {
private:
int value;
public:
void setValue(int value) {
    this->value = value; // 使用this指针区分成员变量和参数
}
void printValue() {
    std::cout << "Value: " << this->value << std::endl;
}
};

int main() {
    MyClass obj;
    obj.setValue(42);
    obj.printValue(); // 输出:Value: 42
    return 0;
}

5.2. 成员函数重载

成员函数重载是指在同一个类中定义多个同名函数,但参数列表不同。

#include <iostream>

class Math {
public:
int add(int a, int b) {
    return a + b;
}
double add(double a, double b) {
    return a + b;
}
};
int main() {
    Math math;
    std::cout << math.add(2, 3) << std::endl;       // 输出:5
    std::cout << math.add(2.5, 3.5) << std::endl;   // 输出:6
    return 0;
}

练习

题目1:class与struct的默认访问权限

要求:定义一个class和一个struct,它们各自包含一个整型成员变量和一个成员函数用于设置该变量的值。在main函数中创建这两个类型的对象,并尝试从类外部直接访问它们的成员变量。观察编译器的行为并解释原因。

验收标准:

class的成员变量在类外部无法直接访问,需要通过成员函数来访问。

struct的成员变量在类外部可以直接访问(如果未设置访问权限为private)。

#include <iostream>

using std::cout;
using std::endl;
using std::cin;

class MyClass {
    int i = 1;
public:
    int getMember() {
        return i;
    }
    void setMember(int j) {
        i = j;
    }
};
struct MyStruct {
    int i = 1;
};
int main() {
    MyClass obj;
    MyStruct obj1;
    obj1.i = 10;
    cout << obj1.i << endl;
    // obj.i = 100; errpr: 'int MyClass::i' is private within this context
    // struct成员默认访问权限和继承权限为public,private的默认访问权限和继承权限为private
    cout << obj.getMember() << endl;
    obj.setMember(12);
    cout << obj.getMember() << endl;
    return 0;
}

题目2:继承与访问权限

要求:定义一个基类Base(使用class或struct均可),它包含一个public的整型成员变量。然后定义一个派生类Derived,它继承自Base,但不修改继承方式(默认继承)。在main函数中创建Derived类的对象,并尝试访问基类中的成员变量。

验收标准:

如果基类使用class定义,则派生类无法直接访问基类的成员变量(除非显式声明为public继承)。

如果基类使用struct定义,则派生类可以直接访问基类的成员变量。

#include <iostream>

using std::cout;
using std::endl;
using std::cin;

class MyClass {
public:
    int i = 1;
};

class Son1: MyClass {};
struct Son2: MyClass {};
int main() {
    Son1 cs;
    Son2 ss;
    // cout << cs.i<<endl;  error: 'int MyClass::i' is inaccessible within this context
    // struct成员默认访问权限和继承权限为public,private的默认访问权限和继承权限为private
    cout << ss.i<<endl;

    return 0;
}

题目3:构造函数与析构函数

要求:定义一个class,它包含一个动态分配的整型数组作为成员。为这个类实现构造函数来初始化数组,并实现析构函数来释放数组。在main函数中创建该类的对象,并在对象生命周期结束后观察内存是否被正确释放。

验收标准:

构造函数应正确初始化动态分配的数组。

析构函数应释放动态分配的内存,防止内存泄露。

#include <iostream>

using std::cout;
using std::endl;
using std::cin;

class MyClass {
public:
    MyClass():array(new int[10]) {
        cout << "MyClass::MyClass()" << endl;
        for (int i = 0; i < 10; i++) {
            array[i] = i;
        }
    }
    ~MyClass() {
        cout << "Destructing MyClass" << endl;
        if (array != nullptr) {
            delete[] array;
            array = nullptr;
        }
    }
private:
    int* array = nullptr;
};
void test() {
    MyClass m;
}
int main() {
    test();
    return 0;
}

题目4:定义并使用类

要求:定义一个Person类,包含姓名(name)和年龄(age)作为私有成员变量,以及相应的构造函数、析构函数、设置和获取成员变量的成员函数。在main函数中创建Person对象,并测试这些成员函数。

验收标准:

类定义正确,包含私有成员变量和公有成员函数。

构造函数和析构函数被正确调用。

成员变量的值能够通过公有成员函数正确设置和获取。

#include <iostream>
#include <string>
using std::cout;
using std::endl;
using std::cin;
using std::string;

class Person {
public:
    Person(const string &name, int age):name(name), age(age) {}
    ~Person() {}
    void setName(const string &name) { this->name = name; }
    void setAge(int age) { this->age = age; }
    string getName() const { return this->name; }
    int getAge() const { return this->age; }
    void prinInfo() const;
private:
    string name;
    int age;
};
void Person::prinInfo() const {
    cout << "Name: " << name << ", Age: " << age << endl;
    return;
}
void printPersonInfo(const Person& p) {
    cout << "Name: " << p.getName() << ", Age: " << p.getAge() << endl;
    return;
}

void test() {
    Person liziyi("liziyi", 12);

    printPersonInfo(liziyi);
    liziyi.prinInfo();

    liziyi.setAge(22);
    liziyi.setName("TimothyLi");
    printPersonInfo(liziyi);

    return;
}
int main() {
    test();
    return 0;
}

题目5:实现继承

要求:定义一个Student类,继承自Person类(见题目1),并添加学号(studentID)作为私有成员变量,以及相应的构造函数、析构函数、设置和获取学号的成员函数。在main函数中创建Student对象,并测试这些成员函数。

验收标准:

Student类正确继承自Person类。

Student类能够正确设置和获取Person类的成员变量以及自己的学号。

#include <iostream>
#include <string>
using std::cout;
using std::endl;
using std::cin;
using std::string;

class Person {
public:
    Person(const string &name, int age):name(name), age(age) {}
    ~Person() {}
    void setName(const string &name) { this->name = name; }
    void setAge(int age) { this->age = age; }
    string getName() const { return this->name; }
    int getAge() const { return this->age; }
    virtual void prinInfo()const;
private:
    string name;
    int age;
};

class Student:public Person {
public:
    Student(const string &name, int age, const string id):Person(name, age), ID(id) {}
    ~Student() {}
    void setID(const string id) { this->ID = id; }
    string getID() const { return this->ID; }
    void prinInfo() const override;
private:
    string ID;
};
void Student::prinInfo() const{
    cout << "Name: " << getName();
    cout << ", Age: " << getAge();
    cout << ", ID: " << getID() << endl;
};

void Person::prinInfo() const{
    cout << "Name: " << name << ", Age: " << age << endl;
    return;
}


void test() {
    Student liziyi("TimothyLi", 12, "123");
    liziyi.prinInfo();
    liziyi.setID("66");
    liziyi.prinInfo();

    Person *p = &liziyi;
    p->prinInfo();

    return;
}
int main() {
    test();
    return 0;
}

题目6:实现多态

要求:定义一个基类Shape,包含至少一个纯虚函数draw()。然后定义两个派生类Circle和Rectangle,分别实现draw()函数以绘制圆形和矩形。在main函数中,通过基类指针数组来存储不同形状的对象,并调用它们的draw()函数。

验收标准:

基类Shape包含纯虚函数draw()。

派生类Circle和Rectangle正确实现了draw()函数。

在main函数中,通过基类指针数组能够正确调用不同派生类对象的draw()函数,实现多态。

#include <iostream>
#include <string>
using std::cout;
using std::endl;
using std::cin;
using std::string;

class Shape {
public:
    virtual void draw() = 0;
};

class Rectangle : public Shape {
public:
    virtual void draw() {
        cout << "draw rectangle" << endl;
    };
};

class Circle : public Shape {
public:
    virtual void draw() {
        cout << "draw circle" << endl;
    }
};

void test() {
    Shape *s1 = new Rectangle;
    Shape *s2 = new Circle;
    s1->draw();
    s2->draw();
    delete s1;
    delete s2;

    return;
}
int main() {
    test();
    return 0;
}
-