C++面向对象编程学习
- 前言
- 一、C++面向对象编程
- 二、知识点学习
- 1. 定义一个类
- 1.1 使用struct定义
- 1.2 使用class定义
- 1.3 struct和class的区别
- 2. 类的定义方式
- 2.1 单文件定义(Inline Definition)
- 2.2 分离定义(Separate Definition)
- 2.3 头文件守卫
- 3. 访问修饰符
- 4. 构造函数与析构函数
- 4.1 构造函数(Constructor)
- 4.2 析构函数(Destructor)
- 5. this指针
- 6. 虚函数
- 7. 继承
- 8. 其余内容
- 总结
前言
注意:本文只适合有C++基础的朋友食用!另本文章目的在于记录学习C++面向编程学习的过程,会引用到其他人的文章。
推荐文章:【C++核心】一文理解C++面向对象(超级详细!)(作者:数据知道)
一、C++面向对象编程
先由一段C++面向对象编程的典例的代码引入学习:
#include <iostream>
#include <string>// 基类:Animal
class Animal {
public: // 访问权限// 构造函数Animal(std::string name) : name_(name) {}// 虚析构函数virtual ~Animal() {}// 虚函数,用于发声virtual void makeSound() const = 0; // 纯虚函数// 打印动物的名字void printName() const {std::cout << "My name is " << name_ << std::endl;}protected:std::string name_; // 动物的名字
};// 派生类(子类):Dog
class Dog : public Animal {
public:// 构造函数Dog(std::string name) : Animal(name) {}// 实现基类的纯虚函数void makeSound() const override {std::cout << name_ << " says: Bark!" << std::endl;}
};// 派生类:Cat
class Cat : public Animal {
public:// 构造函数Cat(std::string name) : Animal(name) {}// 实现基类的纯虚函数void makeSound() const override {std::cout << name_ << " says: Meow!" << std::endl;}
};// 函数模板,用于处理任何类型的Animal
template <typename AnimalType>
void makeAnimalSound(const AnimalType& animal) {animal.makeSound();
}int main() {Dog dog("Rex");Cat cat("Whiskers");dog.printName();dog.makeSound();cat.printName();cat.makeSound();// 使用模板函数makeAnimalSound(dog);makeAnimalSound(cat);return 0;
}
二、知识点学习
1. 定义一个类
在C++中,struct
和class
关键字都可以用来定义一个类,但它们在默认的访问权限上有所不同。以下是使用struct
和class
定义同一个类的例子:
1.1 使用struct定义
struct MyStruct {int publicData; // 默认为publicprivate:int privateData; // 私有成员public:MyStruct(int value) : privateData(value) {}int getPrivateData() const {return privateData;}void setPrivateData(int value) {privateData = value;}private:void privateMethod() {// 私有方法}
};
1.2 使用class定义
class MyClass {int publicData; // 默认为privateprivate:int privateData; // 私有成员public:MyClass(int value) : privateData(value) {}int getPrivateData() const {return privateData;}void setPrivateData(int value) {privateData = value;}private:void privateMethod() {// 私有方法}
};
1.3 struct和class的区别
- 在
struct
中,如果没有指定访问修饰符,成员默认是public
的。 - 在
class
中,如果没有指定访问修饰符,成员默认是private
的。
从C++11起,你可以使用struct
来定义一个简单的数据结构(没有或很少的行为),而class
通常用于定义具有封装、继承和多态特性的对象(强调一个类型是具有封装、继承和多态的对象)。然而,这只是一个编程习惯上的区分,技术上你可以使用任何一个关键字来定义任何类型的类。
2. 类的定义方式
在C++中,类的定义通常有两种主要方式:一种是在单个文件中定义类的全部内容,另一种是将类的声明(通常称为头文件)和定义(通常称为源文件)分开。
- 单文件定义:通常用于小型项目或简单的类,其中类的实现非常简短,或者为了减少编译依赖。
- 分离定义:通常用于大型项目或复杂的类,其中需要隐藏实现细节,提高代码的可维护性和可读性。
2.1 单文件定义(Inline Definition)
在这种方式中,类的定义被直接放在头文件中,通常是在.h
或.hpp
扩展名的文件中。这种方式被称为内联定义或头文件中的定义。
示例:
// MyClass.h
#ifndef MYCLASS_H
#define MYCLASS_Hclass MyClass {
public:void function() {// 实现细节}
private:int data;
};#endif // MYCLASS_H
在这种方式中,每次包含头文件时,类的定义都会被包含到使用它的每个源文件中。这可能会导致代码重复,但可以减少编译依赖和提高编译速度。
2.2 分离定义(Separate Definition)
就是类的声明放在头文件(.h
或.hpp
),类的(定义)实现放在源文件(.cpp
)。
头文件( MyClass.h ):
// MyClass.h
#ifndef MYCLASS_H
#define MYCLASS_Hclass MyClass {
public:void function();
private:int data;
};#endif // MYCLASS_H
源文件( MyClass.cpp ):
// MyClass.cpp
#include "MyClass.h"void MyClass::function() {// 实现细节data = 42; // 访问私有成员
}
在这种方式中,类的实现被隐藏在源文件中,只有声明被包含在其他文件中。这样做的好处是增加了模块化,减少了重复代码,并且使得单个类的定义更容易维护。
2.3 头文件守卫
这段代码是C++预处理器指令
,用于防止头文件被多次包含,这是一种常见的头文件保护技术。这种技术被称为“包含保护”或“头文件守卫”。下面是每个指令的解释:
-
#ifndef MYCLASS_H
- 这个指令检查一个宏(在这个例子中是MYCLASS_H
)是否还未定义。#ifndef
是“if not defined”的缩写。如果MYCLASS_H
没有定义,那么预处理器会继续处理下面的代码。 -
#define MYCLASS_H
- 这个指令定义了MYCLASS_H
宏。这样,如果这个头文件被包含多次,预处理器会记住MYCLASS_H
已经被定义了,并且跳过第二次及后续的包含。 -
#endif // MYCLASS_H
- 这个指令标记了预处理条件块的结束。#endif
指示预处理器停止处理条件块。注释// MYCLASS_H
是为了提供额外的信息,说明这个#endif
与哪个#ifndef
配对。这是一个编程习惯,有助于在阅读代码时快速识别匹配的指令。
包含保护的目的是防止头文件的内容被编译多次,这可能发生在多个源文件都包含了同一个头文件的情况下。如果没有包含保护,每次头文件被包含时,它的代码都会被重新编译,这不仅会导致编译错误(如重复定义),还可能降低编译效率。
例如,如果你有以下头文件和源文件结构:
// MyClass.h
#ifndef MYCLASS_H
#define MYCLASS_Hclass MyClass {// 类的定义
};#endif // MYCLASS_H// main.cpp
#include "MyClass.h"
#include "MyClass.h" // 第二次包含相同的头文件int main() {MyClass myObject;// 使用myObject
}
在没有包含保护的情况下,MyClass
的定义会被包含两次,导致编译器报错,因为它尝试定义同一个类两次。有了包含保护,预处理器会识别出第二次包含是重复的,并忽略它,从而避免编译错误。
请注意,#endif
指令前的注释// MYCLASS_H
是可选的,但它有助于在阅读代码时快速定位匹配的#ifndef
指令。
3. 访问修饰符
(1)public--------公共的,类外部可以访问;
(2)protected—保护的,类外部不可以访问;
(3)private-------私有的,类外部不能访问;
解析:
-
public
(公共的):- 类外部可以访问:类的公共成员在定义类的外部是可见的,可以直接通过类的实例来访问。
- 示例:
class MyClass { public:int publicData; // 公共成员,可以直接访问void publicMethod() {} // 公共成员函数,可以直接调用 };MyClass obj; obj.publicData = 10; // 直接访问公共成员变量 obj.publicMethod(); // 直接调用公共成员函数
-
protected
(保护的):- 类外部不可以访问:类的受保护成员在定义类的外部是不可见的,不能直接通过类的实例来访问。
- 但可以在派生类中访问:如果一个类派生自具有受保护成员的基类,那么这些受保护的成员在派生类中是可见的。
- 示例:
class Base { protected:int protectedData; // 受保护成员,不能直接访问 };class Derived : public Base { public:void accessBaseMembers() {protectedData = 20; // 在派生类中可以访问基类的受保护成员} };Derived obj; // obj.protectedData = 20; // 错误:在类外部不能直接访问受保护成员 obj.accessBaseMembers(); // 正确:通过派生类的公共接口访问基类的受保护成员
-
private
(私有的):- 类外部不能访问:类的私有成员在定义类的外部是不可见的,不能直接通过类的实例来访问。
- 类内部可以访问:私有成员只能在定义它们的类内部被访问,包括类的成员函数和友元类或友元函数。
- 示例:
class MyClass { private:int privateData; // 私有成员,不能直接访问void privateMethod() {} // 私有成员函数,不能直接调用public:void publicMethod() {privateData = 30; // 在类内部可以访问私有成员privateMethod(); // 在类内部可以调用私有成员函数} };MyClass obj; // obj.privateData = 30; // 错误:在类外部不能直接访问私有成员 // obj.privateMethod(); // 错误:在类外部不能直接调用私有成员函数 obj.publicMethod(); // 正确:通过公共成员函数间接访问私有成员
4. 构造函数与析构函数
在C++中,构造函数和析构函数是特殊的成员函数,它们分别用于初始化对象和清理对象使用资源。
构造函数:
主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。语法格式如下:
构造函数语法:类名(){}
析构函数:
主要作用在于对象销毁前系统自动调用,执行一些清理工作。语法格式如下:
析构函数语法: ~类名(){}
4.1 构造函数(Constructor)
构造函数是一种特殊的成员函数,用于创建类的对象时初始化对象。它具有与类相同的名称,且没有返回类型,也不能被声明为const
、volatile
或static
。构造函数可以有参数,也可以没有参数(无参数构造函数),可以有多个构造函数(构造函数重载),以适应不同的初始化需求。
特点:
- 没有返回值,也不抛出异常(除了析构函数外)。
- 可以有0个或多个参数。
- 可以有默认参数。
- 可以被重载以提供不同的初始化方式。
示例:
class MyClass {
public:// 无参数的构造函数MyClass() {// 初始化代码}// 带参数的构造函数MyClass(int value) {// 使用参数value进行初始化}// 带多个参数的构造函数MyClass(int value, const std::string& name) {// 使用参数value和name进行初始化}
};
4.2 析构函数(Destructor)
析构函数是一种特殊的成员函数,用于对象生命周期结束时执行清理工作。它用于释放对象在生命周期中分配的资源,如动态分配的内存、文件句柄、网络连接等。析构函数的名称是在类名前加上波浪号(~
),且析构函数不能有参数,不能有返回值,也不能被重载。
特点:
- 没有返回值。
- 不能带有参数。
- 不能被重载。
- 一个类只能有一个析构函数。
示例:
class MyClass {
public:// 构造函数MyClass() {// 分配资源,如动态内存}// 析构函数~MyClass() {// 释放资源,如动态内存}
};
在上述示例中,当使用new
创建MyClass
对象时,会调用构造函数进行初始化。当对象不再需要时,比如超出作用域或者使用delete
时,会调用析构函数进行清理。
5. this指针
在C++中,this
指针是一个特殊的指针,它在每个非静态成员函数内部可用。this
指针指向调用成员函数的对象本身,它允许成员函数访问对象的数据成员和成员函数。
作用:
-
访问对象的成员:成员函数经常需要访问调用它的那个对象的成员变量和其他成员函数。
this
指针使得成员函数能够区分不同的对象,即使它们拥有相同的成员名称。 -
链式调用:
this
指针可以用来返回对象本身,从而实现链式调用。 -
区分成员变量和局部变量:当成员函数中的局部变量与类的成员变量同名时,
this
指针可以用来区分它们。
示例:
class MyClass {int value;public:MyClass(int val) : value(val) {}int getValue() const {return value; // 使用this指针访问成员变量}MyClass& set(int val) { // 成员函数返回对象的引用this->value = val;return *this; // 使用this指针返回对象本身}void printValue() const {int localValue = 10; // 局部变量int* memVarPtr = &this->value; // 使用this指针区分成员变量和局部变量int* localVarPtr = &localValue;// 输出指针地址以区分成员变量和局部变量std::cout << "Member variable value: " << *memVarPtr << std::endl;std::cout << "Local variable value: " << *localVarPtr << std::endl;}
};int main() {MyClass obj(100);obj.printValue(); // 输出对象的成员变量和局部变量的值obj.set(200).set(300).set(400); // 链式调用std::cout << "Final value: " << obj.getValue() << std::endl; // 输出最终值return 0;
}
注意事项:
this
指针在静态成员函数中不可用,因为静态成员函数不依赖于具体的对象实例。this
指针在普通成员函数中是隐式定义的,不需要声明。this
指针通常不需要手动管理,但了解其存在和作用对于编写更清晰和更有效的代码是有帮助的。- 在异常情况下,如果成员函数被非法调用(例如,在一个未初始化的对象上),
this
指针可能不是一个有效的指针,因此在处理异常和错误时需要小心。
6. 虚函数
语法格式:
virtual ReturnType FunctionName(Parameters) {// 函数实现
}
这里的virtual
关键字告诉编译器这个函数是虚函数,可以被子类重写。
当通过基类指针或引用调用虚函数时,如果对象实际是派生类的实例,那么将调用派生类中的版本,这就是多态。如果对象是基类实例,则调用基类中的版本。
例如:
class Base {
public:virtual void show() {cout << "Base class show()" << endl;}
};class Derived : public Base {
public:void show() override { // C++11中引入了override关键字来明确指出函数重写了基类中的虚函数cout << "Derived class show()" << endl;}
};int main() {Base* b;Derived d;b = &d; // 基类指针指向派生类对象b->show(); // 输出Derived class show(),展示了多态性return 0;
}
在这个例子中,尽管b
是基类Base
的指针,但由于它指向的是派生类Derived
的对象,所以调用的实际上是Derived
类中的show
方法。
7. 继承
语法:class A : public B;
MyFrame::MyFrame() 表示MyFrame()是MyFrame的构造函数。
继承的主要优点包括:
- 代码重用:可以减少代码冗余,提高开发效率。
- 扩展性:可以基于现有的类进行扩展,增加新的功能。
- 维护性:修改基类会影响到所有派生类,简化了维护工作。
在C++中,继承是通过在类定义时使用:
和继承类型关键字(如public
, protected
, private
)来实现的。例如:
class Base {
public:void show() {cout << "Base class show()" << endl;}
};class Derived : public Base { // 使用public继承
public:void display() {cout << "Derived class display()" << endl;}
};int main() {Derived d;d.show(); // 继承自Base类的show方法d.display(); // Derived类特有的display方法return 0;
}
在这个例子中,Derived
类通过public
关键字继承了Base
类。这意味着Derived
类的对象可以访问Base
类中所有公有的成员。
继承类型关键字决定了基类成员在派生类中的访问级别:
public
继承:基类中的公有成员和保护成员在派生类中仍然是公有的和保护的。protected
继承:基类中的公有成员和保护成员在派生类中都变成保护的。private
继承:基类中的公有成员和保护成员在派生类中都变成私有的。
8. 其余内容
推荐文章:【C++核心】一文理解C++面向对象(超级详细!)(作者:数据知道)
总结
本来学的C++没有学到面向对象编程这块,应为主要用来刷算法题,但由于最近一个项目学习需要到此方面知识,故来学习一下。