C++的类
C++的类具有以下特性:
-
封装(Encapsulation):封装是指将数据(变量)和操作数据的函数绑定在一起,形成一个整体——对象。这可以隐藏对象的内部结构,只对外公开有限的接口。
-
继承(Inheritance):继承是指新的类可以继承已有类的属性和行为。这使得新的类可以复用已有类的代码,减少冗余。
-
多态(Polymorphism):多态是指同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。在C++中,多态可以通过虚函数(virtual function)和模板(template)来实现。
-
抽象(Abstraction):抽象是指只展示系统的关键特征,而不展示细节。在C++中,可以通过类(class)和接口(interface)来实现抽象。
-
构造函数和析构函数:构造函数用于初始化类的对象,析构函数用于在对象生命周期结束时进行清理工作。
-
类的成员:类的成员包括数据成员(也叫属性)和成员函数(也叫方法)。数据成员可以是任何数据类型,包括其他类的对象。成员函数可以用来操作数据成员。
-
访问控制:类的成员可以被声明为public、private或protected。public成员可以在任何地方被访问,private成员只能在类的内部被访问,protected成员可以在类的内部和子类中被访问。
-
类的静态成员:类的静态成员是属于类本身的,而不是类的对象。静态成员在所有的对象中是共享的。
-
运算符重载: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类将数据成员width和height以及操作这些数据的成员函数set_values和area封装在一起。数据成员是私有的,只能通过成员函数来访问和修改。这样做的好处是可以控制对数据的访问,避免数据被外部代码错误地修改。同时,如果以后需要修改数据的存储方式或操作方式,只需要修改成员函数,而不需要修改使用这个类的其他代码。这就是封装的概念。
下面是一个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类的所有方法(例如:eat和sleep),并且还可以有自己的方法(例如:bark)。这样,Dog类可以复用Animal类的代码,而不需要重复编写eat和sleep这样的方法。这就是继承的概念。
以下是一个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类定义了一个虚函数sound,Dog类和Cat类分别重写了这个函数。因此,当我们使用指向Dog或Cat对象的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类是一个抽象类,它定义了一个纯虚函数draw。Circle类和Rectangle类是从Shape类派生的,它们分别重写了draw函数。因此,我们可以使用Shape指针来操作Circle对象和Rectangle对象,调用正确的draw函数。这就是抽象的概念。
在C++中,纯虚函数是一种特殊的虚函数。在基类中,纯虚函数没有定义,只有声明,并且在声明后面加上=0。这表示这个函数是纯虚函数,它在基类中没有定义,必须在派生类中被重写。
class AbstractClass {
public:
virtual void pureVirtualFunction() = 0; // 纯虚函数
};
包含纯虚函数的类被称为抽象类。抽象类不能被实例化,只能作为基类,由派生类继承,并在派生类中实现纯虚函数。这提供了一种强制派生类实现特定函数的方法。
例如,如果我们有一个Shape类,它有一个draw的纯虚函数,那么任何从Shape派生的类(如Circle,Rectangle等)都必须提供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(); 这两种方式都可以用来创建对象,但它们的区别在于对象的存储位置和生命周期。
-
MyClass obj1;创建的对象存储在栈上。当程序退出当前作用域时,栈上的对象会被自动销毁,对象的内存会被自动回收。这种方式创建的对象,我们不需要手动管理内存,但是对象的生命周期受到作用域的限制。 -
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++中,双冒号(::)被称为"作用域解析运算符"。
它主要有以下几种用途:
-
访问类或命名空间中的静态成员。例如,如果你有一个类MyClass,该类有一个静态成员myMember,你可以通过MyClass::myMember来访问这个静态成员。
-
在类的外部定义成员函数。例如,如果你在类的定义中只声明了一个函数,并且打算在类定义外部定义这个函数的实现,你需要使用双冒号来指明这个函数属于哪个类。例如,如果你有一个类MyClass,并且你在类外部定义一个成员函数myFunction,你需要这样写:void MyClass::myFunction() { /* 函数体 */ }。
-
指定父类的成员函数或变量。如果一个派生类中的函数覆盖了一个基类中的函数,那么在派生类中可以使用双冒号来调用基类的版本。例如,如果你有一个基类Base和一个派生类Derived,Derived覆盖了Base的一个函数myFunction,那么在Derived的成员函数中,你可以通过Base::myFunction()来调用Base版本的myFunction。
-
访问全局作用域的函数或变量。如果一个局部作用域中有一个和全局作用域中同名的变量或函数,那么在局部作用域中可以使用双冒号来访问全局作用域的版本。例如,如果你有一个全局变量myVariable和一个局部变量myVariable,那么在这个局部作用域中,你可以通过::myVariable来访问全局的myVariable。