devops icon indicating copy to clipboard operation
devops copied to clipboard

C++的类

Open heidsoft opened this issue 2 years ago • 0 comments

C++的类具有以下特性:

  1. 封装(Encapsulation):封装是指将数据(变量)和操作数据的函数绑定在一起,形成一个整体——对象。这可以隐藏对象的内部结构,只对外公开有限的接口。

  2. 继承(Inheritance):继承是指新的类可以继承已有类的属性和行为。这使得新的类可以复用已有类的代码,减少冗余。

  3. 多态(Polymorphism):多态是指同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。在C++中,多态可以通过虚函数(virtual function)和模板(template)来实现。

  4. 抽象(Abstraction):抽象是指只展示系统的关键特征,而不展示细节。在C++中,可以通过类(class)和接口(interface)来实现抽象。

  5. 构造函数和析构函数:构造函数用于初始化类的对象,析构函数用于在对象生命周期结束时进行清理工作。

  6. 类的成员:类的成员包括数据成员(也叫属性)和成员函数(也叫方法)。数据成员可以是任何数据类型,包括其他类的对象。成员函数可以用来操作数据成员。

  7. 访问控制:类的成员可以被声明为public、private或protected。public成员可以在任何地方被访问,private成员只能在类的内部被访问,protected成员可以在类的内部和子类中被访问。

  8. 类的静态成员:类的静态成员是属于类本身的,而不是类的对象。静态成员在所有的对象中是共享的。

  9. 运算符重载:C++允许在类中重新定义或重载大部分的C++内置运算符。这使得我们可以使用自然的语法形式对类的对象执行运算。

这些特性使得C++的类非常强大和灵活,可以用来解决各种复杂的编程问题。

下面是一个简单的C++代码示例,展示了封装的概念。

#include <iostream>

class Rectangle {
private:
    int width, height; // 私有数据成员,外部无法直接访问

public:
    // 公有成员函数,提供对私有数据的操作
    void set_values(int w, int h) {
        width = w;
        height = h;
    }
    int area() {
        return width * height;
    }
};

int main() {
    Rectangle rect;
    rect.set_values(5, 6); // 通过公有函数设置私有数据
    std::cout << "Area: " << rect.area(); // 输出:Area: 30
    return 0;
}

在这个例子中,Rectangle类将数据成员widthheight以及操作这些数据的成员函数set_valuesarea封装在一起。数据成员是私有的,只能通过成员函数来访问和修改。这样做的好处是可以控制对数据的访问,避免数据被外部代码错误地修改。同时,如果以后需要修改数据的存储方式或操作方式,只需要修改成员函数,而不需要修改使用这个类的其他代码。这就是封装的概念。

下面是一个C++代码示例,展示了继承的概念。

#include <iostream>

class Animal {
public:
    void eat() {
        std::cout << "I can eat!" << std::endl;
    }
    void sleep() {
        std::cout << "I can sleep!" << std::endl;
    }
};

// Dog 类从 Animal 类继承
class Dog : public Animal {
public:
    void bark() {
        std::cout << "I can bark! Woof! Woof!" << std::endl;
    }
};

int main() {
    Dog myDog;
    myDog.eat();  // 从 Animal 类继承的方法
    myDog.sleep(); // 从 Animal 类继承的方法
    myDog.bark(); // Dog 类自己的方法
    return 0;
}

在这个例子中,Dog类是从Animal类继承的。这意味着Dog类具有Animal类的所有方法(例如:eatsleep),并且还可以有自己的方法(例如:bark)。这样,Dog类可以复用Animal类的代码,而不需要重复编写eatsleep这样的方法。这就是继承的概念。

以下是一个C++代码示例,展示了多态的概念。

#include <iostream>

class Animal {
public:
    // 虚函数
    virtual void sound() {
        std::cout << "Some sound..." << std::endl;
    }
};

class Dog : public Animal {
public:
    // 重写虚函数
    void sound() override {
        std::cout << "Woof! Woof!" << std::endl;
    }
};

class Cat : public Animal {
public:
    // 重写虚函数
    void sound() override {
        std::cout << "Meow! Meow!" << std::endl;
    }
};

int main() {
    Animal* myAnimal = new Animal();
    Animal* myDog = new Dog();
    Animal* myCat = new Cat();

    myAnimal->sound(); // 输出:Some sound...
    myDog->sound();    // 输出:Woof! Woof!
    myCat->sound();    // 输出:Meow! Meow!

    delete myAnimal;
    delete myDog;
    delete myCat;

    return 0;
}

在这个例子中,Animal类定义了一个虚函数soundDog类和Cat类分别重写了这个函数。因此,当我们使用指向DogCat对象的Animal指针调用sound函数时,会根据实际的对象类型调用对应的函数。这就是多态的概念。

在C++中,虚函数是一种特殊的成员函数,它在基类中被声明为virtual,并在派生类中重新定义。虚函数的主要用途是实现多态。

多态是面向对象程序设计中的一个重要特性,它允许我们使用一个基类指针来操作派生类对象。在没有虚函数的情况下,如果我们用基类指针调用一个函数,那么调用的总是基类的版本,而不是派生类的版本。但是如果这个函数在基类中被声明为虚函数,那么调用的就是派生类的版本(只要这个函数在派生类中被重新定义)。

这就是虚函数的主要作用:它允许我们通过基类指针调用派生类的函数,从而实现多态。

以下是虚函数的一个典型使用场景:

假设我们正在编写一个图形编辑器,有一个基类Shape,代表所有的图形,有一个虚函数draw(),用于绘制图形。然后有多个派生类,如Circle、Rectangle等,每个派生类都重新定义了draw()函数,用于绘制特定的图形。

在这种情况下,我们可以创建一个Shape指针数组,用来存储所有的图形。然后我们可以遍历这个数组,对每个元素调用draw()函数,来绘制所有的图形。由于draw()是虚函数,所以调用的总是正确的版本,即对于Circle对象,调用的是Circle的draw(),对于Rectangle对象,调用的是Rectangle的draw()。如果draw()不是虚函数,那么调用的总是Shape的draw(),无论实际的对象是什么,这显然是不对的。

因此,虚函数是实现多态的一个重要工具,它使得我们可以使用基类指针来操作派生类对象,提高了代码的灵活性和可扩展性。

在C++中,抽象是通过抽象类(abstract class)和接口(interface)实现的。抽象类是一种特殊的类,它包含至少一个纯虚函数(pure virtual function)。接口是一种完全抽象的类,它的所有函数都是纯虚函数。抽象类和接口都不能直接实例化,它们主要是用来定义接口,即定义派生类应该具有的行为。

以下是一个C++的代码示例,展示了抽象的概念:

#include <iostream>

// 抽象类 Shape
class Shape {
public:
    // 纯虚函数
    virtual void draw() = 0;
};

// Circle 类继承自 Shape
class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Draw a circle" << std::endl;
    }
};

// Rectangle 类继承自 Shape
class Rectangle : public Shape {
public:
    void draw() override {
        std::cout << "Draw a rectangle" << std::endl;
    }
};

int main() {
    // Shape shape;  // 错误:不能实例化抽象类
    Shape* shape1 = new Circle();
    Shape* shape2 = new Rectangle();

    shape1->draw();  // 输出:Draw a circle
    shape2->draw();  // 输出:Draw a rectangle

    delete shape1;
    delete shape2;

    return 0;
}

在这个例子中,Shape类是一个抽象类,它定义了一个纯虚函数drawCircle类和Rectangle类是从Shape类派生的,它们分别重写了draw函数。因此,我们可以使用Shape指针来操作Circle对象和Rectangle对象,调用正确的draw函数。这就是抽象的概念。

在C++中,纯虚函数是一种特殊的虚函数。在基类中,纯虚函数没有定义,只有声明,并且在声明后面加上=0。这表示这个函数是纯虚函数,它在基类中没有定义,必须在派生类中被重写。

class AbstractClass {
public:
    virtual void pureVirtualFunction() = 0; // 纯虚函数
};

包含纯虚函数的类被称为抽象类。抽象类不能被实例化,只能作为基类,由派生类继承,并在派生类中实现纯虚函数。这提供了一种强制派生类实现特定函数的方法。

例如,如果我们有一个Shape类,它有一个draw的纯虚函数,那么任何从Shape派生的类(如CircleRectangle等)都必须提供draw函数的实现。这确保了无论形状如何,我们总是可以画出它。

纯虚函数是实现抽象基类(也称为接口)的关键,它定义了派生类应该具有的公共接口,但没有提供默认的实现。这使得不同的派生类可以提供不同的实现,从而实现多态。

你说的完全正确,C++中的静态成员确实属于类本身,而不是类的对象。所有的对象都共享同一个静态成员。这就意味着,无论创建多少个对象,静态成员都只有一份。

以下是一个C++的代码示例,展示了静态成员的使用:

#include <iostream>

class MyClass {
public:
    static int count;  // 静态成员变量

    MyClass() {
        count++;
    }

    static void showCount() {  // 静态成员函数
        std::cout << "Count: " << count << std::endl;
    }
};

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

int main() {
    MyClass::showCount();  // 输出:Count: 0

    MyClass obj1;
    MyClass::showCount();  // 输出:Count: 1

    MyClass obj2, obj3;
    MyClass::showCount();  // 输出:Count: 3

    return 0;
}

在这个例子中,MyClass有一个静态成员变量count和一个静态成员函数showCount。每当创建一个MyClass的对象,构造函数就会使count增加1。然后我们可以通过showCount函数来查看count的值。

注意,静态成员变量需要在类外进行初始化,而且静态成员可以通过类名直接访问,而不需要创建对象。

在 C++ 中,MyClass obj1;MyClass* obj1 = new MyClass(); 这两种方式都可以用来创建对象,但它们的区别在于对象的存储位置和生命周期。

  1. MyClass obj1; 创建的对象存储在栈上。当程序退出当前作用域时,栈上的对象会被自动销毁,对象的内存会被自动回收。这种方式创建的对象,我们不需要手动管理内存,但是对象的生命周期受到作用域的限制。

  2. MyClass* obj1 = new MyClass(); 创建的对象存储在堆上。堆上的对象不会被自动销毁,它会一直存在,直到我们手动删除它。因此,使用 new 创建对象,我们需要手动管理内存,使用 delete 关键字来删除对象并回收内存。如果忘记删除,会导致内存泄漏。

以下是一个简单的例子来说明这两种方式的区别:

void function() {
    MyClass obj1; // 存储在栈上,函数结束时自动销毁

    MyClass* obj2 = new MyClass(); // 存储在堆上,函数结束时不会自动销毁

    // 对 obj2 的操作...

    delete obj2; // 手动删除对象,回收内存
}

总结一下,new 创建的对象提供了更大的灵活性(例如,可以动态地在运行时创建和销毁对象),但是需要我们手动管理内存。而直接创建的对象生命周期受作用域限制,但是不需要我们手动管理内存。

在C++中,new操作符用于在堆上动态创建对象,它返回的是新创建对象的指针。所以当你写MyClass* obj = new MyClass();时,你实际上是声明了一个指向MyClass对象的指针,并使它指向一个新创建的MyClass对象。

所以,*的作用是声明一个指针。在C++中,指针是一种特殊的变量,它存储的是内存地址,而不是常规的数据值。当你声明MyClass* obj;时,obj是一个指针,它可以存储一个MyClass对象的地址。

当你使用new MyClass()时,它在堆上创建一个MyClass对象,并返回这个对象的地址。然后,这个地址被赋值给obj,所以obj现在指向新创建的对象。

这种使用指针和new的方式允许我们动态地在运行时创建对象,而且这些对象的生命周期不受作用域的限制。但是,我们需要手动管理这些对象的内存,确保在对象不再需要时使用delete来释放它们占用的内存。

在 C++ 中,你可以重载(overload)大多数的运算符,使得它们可以用于你自定义的类型。这样可以让你的代码更加直观和易读。以下是一个简单的例子,我们定义了一个复数类(Complex)并重载了加法运算符(+)和输出运算符(<<):

#include <iostream>

class Complex {
public:
    Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}

    // 重载加法运算符
    Complex operator+(const Complex& rhs) const {
        return Complex(real + rhs.real, imag + rhs.imag);
    }

    // 重载插入运算符
    friend std::ostream& operator<<(std::ostream& os, const Complex& c);

private:
    double real, imag;
};

// 定义插入运算符
std::ostream& operator<<(std::ostream& os, const Complex& c) {
    os << c.real << " + " << c.imag << "i";
    return os;
}

int main() {
    Complex c1(1.0, 2.0), c2(2.0, 3.0);
    Complex c3 = c1 + c2; // 使用重载的加法运算符
    std::cout << c3 << std::endl; // 使用重载的插入运算符
    return 0;
}

在这个例子中,Complex operator+(const Complex& rhs) const 是一个成员函数,它重载了加法运算符。friend std::ostream& operator<<(std::ostream& os, const Complex& c); 是一个友元函数,它重载了插入运算符。

运行这段代码,会输出 "3 + 5i",这是复数 (1 + 2i) 和 (2 + 3i) 相加的结果。

在C++中,冒号在类的构造函数中常常用来初始化类的数据成员。在你给出的例子中,Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {},冒号后面的real(r), imag(i)表示将参数r和i的值分别赋给类的成员变量real和imag。这就是冒号在这里的作用。

这种初始化方式叫做成员初始化列表,它是在构造函数体执行之前初始化成员的一种方式。如果不使用初始化列表,那么类的成员将会在构造函数体内被赋值,而不是初始化。对于基本类型,这个区别可能不太明显,但对于某些类型(如const成员,引用成员,没有默认构造函数的类类型成员等),只能使用初始化列表进行初始化。

在C++中,双冒号(::)被称为"作用域解析运算符"。

它主要有以下几种用途:

  1. 访问类或命名空间中的静态成员。例如,如果你有一个类MyClass,该类有一个静态成员myMember,你可以通过MyClass::myMember来访问这个静态成员。

  2. 在类的外部定义成员函数。例如,如果你在类的定义中只声明了一个函数,并且打算在类定义外部定义这个函数的实现,你需要使用双冒号来指明这个函数属于哪个类。例如,如果你有一个类MyClass,并且你在类外部定义一个成员函数myFunction,你需要这样写:void MyClass::myFunction() { /* 函数体 */ }。

  3. 指定父类的成员函数或变量。如果一个派生类中的函数覆盖了一个基类中的函数,那么在派生类中可以使用双冒号来调用基类的版本。例如,如果你有一个基类Base和一个派生类Derived,Derived覆盖了Base的一个函数myFunction,那么在Derived的成员函数中,你可以通过Base::myFunction()来调用Base版本的myFunction。

  4. 访问全局作用域的函数或变量。如果一个局部作用域中有一个和全局作用域中同名的变量或函数,那么在局部作用域中可以使用双冒号来访问全局作用域的版本。例如,如果你有一个全局变量myVariable和一个局部变量myVariable,那么在这个局部作用域中,你可以通过::myVariable来访问全局的myVariable。

heidsoft avatar Sep 30 '23 14:09 heidsoft