突破编程_C++_基础教程(类的基础知识)
1 面向对象
面向对象( Object-Oriented ,简称 OO )是一种编程思想,它使用对象的理念来设计软件和构建程序。面向对象编程( Object-Oriented Programming ,简称 OOP )是这种思想的具体实现方式。在面向对象编程中,程序把对象作为基本单元,对象包含了数据(属性)和操作数据的函数(方法),通过对象之间的交互以及消息传递完成了程序的运行。
1.1 面向对象编程的三大基本特性
面向对象编程的三大基本特性是:封装( Encapsulation )、继承( Inheritance )和多态( Polymorphism )。
(1)封装(Encapsulation)
封装是指将对象的属性和方法隐藏在其内部,只通过有限的接口与外部进行交互。这样可以防止外部代码随意访问和修改对象的内部数据,从而提高代码的安全性和可维护性。
(2)继承(Inheritance)
继承是指一个类(子类)可以继承另一个类(父类)的属性和方法,从而实现代码的重用。子类可以继承父类的所有非私有属性和方法,并可以添加或覆盖父类的方法。 C++ 支持三种类型的继承:公有继承( public )、保护继承( protected )和私有继承( private )。其中,公有继承是最常用的继承方式。
(3)多态(Polymorphism)
多态是指允许一个成员函数被多种数据类型实现(重载),或者一个成员函数在不同场景下有不同实现方式(重写)。多态性允许使用基类的指针或引用来调用派生类中的成员函数,在运行时根据实际对象的类型来确定调用哪个函数,从而增强了程序的灵活性和可扩展性。
1.2 面向对象编程的主要优点
(1)通过继承增强代码的可重用性:可以很方便地重用父类的属性与方法。
(2)通过封装增强代码的可维护性:可以隐藏对象的内部实现细节,只通过有限的接口与外部进行交互,从而降低代码的耦合度,提高代码的可维护性。
(3)通过多态增强代码的可扩展性:可以在不修改现有代码的情况下,增加新的功能或重构现有功能的行为。
1.3 面向过程编程与面向对象编程的区别
(1)设计思路
面向过程以算法为核心,通过分析问题,确定程序流程和模块化的分解。面向对象以对象为核心,通过封装、继承和多态等特性实现代码复用和扩展。它要解决的问题分解成各个对象,各个对象之间交互,完成事件,解决问题。
(2)数据与方法
在面向过程编程中,数据和对数据的操作通常是分开的。而在面向对象编程中,数据和相关的操作被封装在一起形成对象。
(3)实现方式
面向过程编程采用函数调用的方式实现功能。具体来说,它采用模块化、流程化的编程方式,具体步骤和每个步骤需要完成的任务明确清晰,在开发之前基本考虑了实现方式和最终结果,便于节点分析。而面向对象采用对象的方式实现功能。它通过将控制权转移到数据上,实现了数据控制代码的访问。
(4)适用场景
面向过程编程以函数为基本单位,结构化,适用于需要高性能和明确流程的程序开发。面向对象编程以类和对象为基本单位,提供了抽象、封装、继承和多态等好处(比较消耗资源),适用于大型程序开发。
2 类的概念与基本用法
类( Class )是一种用户自定义的数据类型,它允许程序员定义自己的数据类型,并封装数据和方法(即函数)在一起。类提供了数据抽象、封装、继承和多态等面向对象的特性,有助于创建更加模块化和可重用的代码。
2.1 类的语法结构
类的语法结构通常包括类名、访问修饰符( public 、 private 、 protected )、成员变量和成员函数(包括构造函数以及析构函数):
(1)成员变量:也称为属性或字段,用于存储对象的状态或数据。
(2)成员函数:也称为方法或行为,定义了对象可以执行的操作。
(3)构造函数:一种特殊的成员函数,当创建类的对象时自动调用。通常用于初始化对象的属性。
(4)析构函数:一种特殊的成员函数,在对象被销毁之前调用,用于执行清理操作。
(5)访问修饰符:包括 public 、private 、 protected ,用于定义成员变量的可见性和成员函数的访问权限。
下面是一个简单的类的语法结构示例:
class MyClass // 类名
{
// 构造函数与析构函数
public: // 访问修饰符:公有成员(默认情况下,如果不指定访问修饰符,则成员是私有的)
MyClass() {}; // 构造函数
~MyClass() {}; // 析构函数
// 公共成员函数与成员变量
public:
void publicFunc() {} // 公共成员函数
int m_publicVal; // 公共成员变量
// 保护成员函数与成员变量
protected:
void protectedFunc() {} // 保护成员函数
int m_protectedVal; // 保护成员变量
// 私有成员函数与成员变量
private:
void privateFunc() {} // 私有成员函数
int m_privateVal; // 私有成员变量
};
2.2 类的基本用法
根据 " 2.1 类的语法结构 "介绍的语法规则,可以创建如下的一个类:
class Animal
{
public:
Animal(string name):m_name(name) {};
~Animal() {};
public:
void eat()
{
printf("eat something");
}
string getName()
{
return m_name;
}
private:
string m_name;
};
在上面中,Animal 是一个类,它有两个成员函数 eat 以及 getName ,有一个属性 m_name 。在定义这个类之后就可以创建该类的一个实例:
Animal animal("aa");
animal.eat();
在创建该类的实例后,即可使用点操作符调用该对象的成员函数。C++11 引入了列表初始化(也称为统一初始化),它提供了一种更直观、更灵活的方式来初始化对象。列表初始化可以使用花括号 {} 来包围初始化值:
Animal animal{ "aa" };
还可以使用 new 关键字在堆上动态地创建对象。这种方式创建的对象在生命周期结束时需要使用 delete 关键字来释放内存:
Animal* animal = new Animal("aa");
animal->eat();
注意:使用 new 关键字在堆上动态地创建对象需要使用箭头操作符调用该对象的成员函数。
3 类访问修饰符
类访问修饰符用于控制类成员的访问权限,即确定哪些成员可以在类的对象外部访问,哪些只能在类的内部访问。C++提供了三种访问修饰符:public、private和protected。
3.1 public 访问修饰符
使用 public 修饰符声明的成员函数与成员变量是公开的。当创建一个类的对象时,可以直接访问 public 修饰符修饰的成员:
class Animal
{
public:
Animal() {};
~Animal() {};
public:
void eat()
{
printf("eat something");
}
public:
string name;
};
Animal animal;
animal.eat(); // OK:可以访问公有成员函数
eat.name = "aa"; // OK:可以访问公有成员变量
public 访问修饰符主要用于定义那些可以从类的外部访问的成员(包括成员变量和成员函数)。使用 public 访问修饰符可以使类的某些部分对外部代码可见,从而实现类的功能与外部对象的交互。
以下是一些 public 访问修饰符的应用场景:
(1)接口定义
当定义一个类作为接口时,通常会将成员函数声明为 public,以便其子类可以实现这些接口。接口中的成员函数通常是纯虚函数,没有实现,但它们定义了类与外部对象的交互方式。
(2)访问器( getter )和修改器( setter )方法
在面向对象编程中,为了封装数据并控制对数据的访问,通常会使用 public 访问修饰符来声明访问器( getter )和修改器( setter )方法。这些方法允许外部代码读取或修改类的私有数据成员,同时保持对数据的完整性和安全性的控制。
(3)构造函数和析构函数
通常情况下,构造函数和析构函数也会被声明为 public,以便外部代码可以创建和销毁类的对象。如果构造函数或析构函数被声明为 private 或 protected,则外部代码将无法直接创建或销毁类的对象。
(4)操作符重载
当需要对类的操作符进行重载时,这些重载的操作符函数通常也会被声明为 public。这样,外部代码就可以使用这些操作符与类的对象进行交互。
(5)公共工具函数
类中可能包含一些公共工具函数,这些函数不直接操作类的状态,但提供了与类相关的有用功能。这些函数通常也会被声明为 public,以便外部代码可以使用它们。
(6)继承中的可见性
在类的继承关系中,如果基类的成员函数或数据成员被声明为 public,则它们在派生类中也将是可见的,并且可以通过派生类的对象进行访问。这使得派生类可以继承和重用基类的功能。
3.2 private 访问修饰符
使用 private 修饰符声明的成员函数与成员变量是私有的。当将类的成员标记为 private 时,这意味着这个成员只能在它所属的类内部被访问,而不能从类的外部直接访问:
class Animal
{
public:
Animal() {};
~Animal() {};
private:
void eat()
{
printf("eat something");
}
private:
string name;
};
Animal animal;
animal.eat(); // 错误:不可以访问私有成员函数
eat.name = "aa"; // 错误:不可以访问私有成员变量
private 访问修饰符在编程中有多种应用场景,它主要用于限制类成员的访问权限,以确保数据的安全性和封装性。
以下是一些 private 访问修饰符的应用场景:
(1)封装数据
private 修饰符常用于封装类的内部数据。通过将数据成员声明为私有,可以确保只有类自身的方法能够直接访问和修改这些数据,从而防止外部代码对数据的非法访问和修改。
(2)控制访问
使用 private 修饰符可以控制对类成员的访问,从而实现对数据的保护和访问控制。只有类内部的方法可以访问私有成员,而外部代码则需要通过类提供的公共方法来间接访问和操作这些私有成员。
(3)隐藏实现细节
通过将类的实现细节声明为私有,可以隐藏类的内部实现细节,使得类的使用者只关心类的功能和接口,而不需要了解类的具体实现。这有助于保持类的清晰性和可维护性。
3.3 protected 访问修饰符
protected 访问修饰符在设计继承层次结构时特别有用。它允许派生类访问基类的实现细节,同时仍然保持对这些细节的封装和隐藏。这有助于实现更灵活和可维护的代码结构。
注意 protected 和 private 的区别:
如果其他类从该类派生(无论是公开派生还是私有派生),那么派生类中的成员函数可以访问基类中的protected成员。这是 protected 与private 的区别,因为private成员在派生类中是不可访问的。
class Animal
{
public:
Animal() {};
~Animal() {};
protected:
void eat()
{
printf("eat something");
}
protected:
string name;
};
class Dog : public Animal
{
public:
Dog() {};
~Dog() {};
public:
void dogEat()
{
eat(); // OK:派生类中可以直接访问基类的 protected 成员函数
name = "aa"; // OK:派生类中可以直接访问基类的 protected 成员变量
}
protected:
string name;
};
Dog dog;
dog.eat(); // 错误:不可以访问 protected 成员函数
dog.name = "aa"; // 错误:不可以访问 protected 成员变量
protected 访问修饰符的应用场景主要涉及到继承和多层次的对象关系:
(1)封装类层次结构中的状态
如果需要在类层次结构中的多个层次之间共享某些状态信息,但又不想让这些状态信息暴露给类的外部。在这种情况下,可以将这些状态信息声明为 protected,这样它们就可以在类层次结构中的任何层次中被访问和修改。
(2)实现受保护的接口
如果需要定义一个受保护的接口,只有派生类可以实现它。在这种情况下,可以将接口中的方法声明为 protected,这样只有派生类可以覆盖这些方法。
(3)控制数据成员的访问
与 private 修饰符相比, protected 修饰符提供了更灵活的数据成员访问控制。有时,可能需要派生类能够直接访问或修改数据成员,但又不想让类的外部代码这样做。在这种情况下,可以将数据成员声明为 protected 。
3.4 访问修饰符权限总结
本类 | 继承类 | 其他 | |
---|---|---|---|
private | √ | × | × |
protected | √ | √ | × |
public | √ | √ | √ |
4 类的构造函数与析构函数
C++ 类的构造函数和析构函数是特殊的成员函数,它们在创建和销毁类的对象时自动执行。
4.1 构造函数
根据构造函数的参数列表和特性,可以将构造函数分为几种不同的类型。以下是 C++ 中常见的构造函数:
4.1.1 默认构造函数
默认构造函数是一种没有参数的构造函数。如果类中没有定义任何构造函数,编译器会自动生成一个默认构造函数。如果类定义了其他构造函数,但没有定义默认构造函数,编译器就不会自动生成默认构造函数,如下为样例代码:
class Animal1
{
};
class Animal2
{
public:
Animal2(string name) {}
};
Animal1 animal1; // OK:类中没有定义任何构造函数,编译器会自动生成一个默认构造函数
Animal2 animal2; // 错误:类定义了其他构造函数( Animal2(string name) ),但没有定义默认构造函数,编译器就不会自动生成默认构造函数
Animal2 animal2("aa"); // OK:
4.1.2 参数化构造函数
参数化构造函数是带有参数的构造函数。它可以有一个或多个参数,用于在创建对象时初始化对象的成员变量。如下为样例代码:
class Animal
{
public:
Animal(string name) {}
};
Animal animal("aa"); // OK:
4.1.3 拷贝构造函数
拷贝构造函数是一种特殊的构造函数,它使用一个已存在的对象来初始化新创建的对象。它的参数是对同类型对象的常量引用。如果类没有显式定义拷贝构造函数,编译器会提供一个默认的拷贝构造函数。如下为样例代码:
#include <iostream>
#include <string>
using namespace std;
class Animal
{
public:
Animal()
{
printf("call Animal()\n");
}
Animal(const Animal& animal)
{
printf("call Animal(const Animal& animal)\n");
}
};
int main() {
Animal animal1; // 调用无参数的构造函数
Animal animal2(animal1); // 调用拷贝构造函数
return 0;
}
上面代码的输出为:
call Animal()
call Animal(const Animal& animal)
4.1.4 列表初始化构造函数
列表初始化构造函数使用成员初始化列表来初始化对象的数据成员,C++11 的列表初始化特性需要该种类型构造函数的支持。如下为样例代码:
#include <iostream>
#include <string>
using namespace std;
class Animal
{
public:
Animal(const string& name) : m_name(name)
{
printf("call Animal(const string& name) : m_name(name)\n");
}
private:
string m_name;
};
int main() {
Animal animal{"aa"}; // 调用列表初始化构造函数
return 0;
}
上面代码的输出为:
call Animal(const string& name) : m_name(name)
4.1.5 委托构造函数
委托构造函数是一种特殊的构造函数,它调用同类的另一个构造函数来执行初始化。这可以通过使用类名并跟随参数列表来实现。C++11 及以后的版本支持委托构造函数。如下为样例代码:
#include <iostream>
#include <string>
using namespace std;
class Animal
{
public:
Animal() : Animal("default")
{
printf("Animal() : Animal(\"default\")\n");
}
Animal(const string& name) : m_name(name)
{
printf("call Animal(const string& name) : m_name(name)\n");
}
private:
string m_name;
};
int main() {
Animal animal;
return 0;
}
上面代码的输出为:
call Animal(const string& name) : m_name(name)
Animal() : Animal("default")
注意:先调用的是委托构造函数。
4.1.6 移动构造函数
移动构造函数是一种特殊的构造函数,它使用右值引用参数将资源从一个对象移动到另一个对象,而不是复制。这通常用于优化性能,特别是在处理如 vector 、 string 等可能包含动态分配资源的类型时。如下为样例代码:
#include <iostream>
#include <string>
using namespace std;
class Animal
{
public:
Animal()
{
printf("call Animal()\n");
}
Animal(const Animal& animal)
{
printf("call Animal(const Animal& animal)\n");
}
Animal(Animal&& other) noexcept //移动构造函数
{
printf("call Animal(Animal&& other)\n");
}
};
int main() {
Animal animal1; // 调用无参数构造函数
Animal animal2 = animal1; // 调用拷贝构造函数
Animal animal3 = move(animal1); // 调用移动构造函数
return 0;
}
上面代码的输出为:
call Animal()
call Animal(const Animal& animal)
call Animal(Animal&& other)
4.2 析构函数
析构函数是一种特殊的成员函数,它在每次删除所创建的对象时执行。析构函数的名称是在类名前加上一个波浪符( ~ )。析构函数用于执行任何必要的清理任务,如释放对象可能拥有的动态内存或关闭打开的文件等。
析构函数没有返回类型,也没有参数。一个类只能有一个析构函数,并且它不能被重载。当对象的生命周期结束时,析构函数会自动被调用:
#include <iostream>
class Animal
{
public:
~Animal() // 析构函数
{
printf("call ~Animal()\n");
}
};
int main() {
{
Animal animal;
} // 作用域结束后,在销毁 animal 时自动调用其析构函数
return 0;
}
上面代码的输出为:
call ~Animal()
注意,析构函数不应该抛出异常:
如果析构函数抛出异常且没有被捕获,程序将调用 std::terminate 并终止执行。这是因为析构函数在对象生命周期结束时被调用,此时对象可能已经不再处于有效状态,因此处理异常可能不安全。
4.3 构造函数与析构函数在 RAII 中的作用
RAII( Resource Acquisition Is Initialization ,资源获取即初始化)是 C++ 语言中的一种管理资源、避免泄漏的惯用法。其核心理念是将资源的获取和释放与对象的构造和析构绑定在一起。这意味着,当一个对象被创建时,它的构造函数会自动获取所需的资源,并在对象被销毁时,其析构函数会自动释放所占用的资源。
首先,构造函数在 RAII 中的作用是在对象创建时获取所需的资源。这通常包括分配内存、打开文件、建立数据库连接等。构造函数的调用是在对象创建时自动发生的,因此它是确保资源正确获取的关键环节。如果资源获取失败,构造函数可以抛出异常来指示错误。
接着,析构函数在RAII中的作用是在对象销毁时释放所占用的资源。这包括释放内存、关闭文件、销毁数据库连接等。与构造函数类似,析构函数的调用也是在对象生命周期结束时自动发生的,这确保了资源的正确释放,从而避免了资源泄漏的问题。析构函数应该仔细管理资源的释放过程,确保不会出现任何错误,并且不应该抛出异常。
通过将资源的获取和释放与对象的构造和析构绑定在一起, RAII 提供了一种简洁、安全、实时的资源管理方式。
5 this 指针
this 是一个特殊的指针,它代表对象自身。this 指针是隐式传递给每个成员函数的,它允许成员函数访问和修改调用它的对象的成员。this 指针在成员函数内部是自动可用的,不需要显式声明。它通常用于区分成员变量和参数名称相同的情况,或者在函数内部引用当前对象的其他成员:
class Animal
{
public:
Animal(string name)
{
//name = name; // 如果这里不使用 this ,则无法分别这个变量 name 是入参还是成员变量
this->name = name;
}
public:
string getName()
{
return this->name; // 此处的 this 可以用也可以不用
}
private:
string name;
};
在上面的代码中,构造函数 Animal(string name)
和成员函数 getName()
都使用了 this 指针。在构造函数 Animal(string name)
中,this->name
表示对象的 name
成员变量,而构造函数的入参也是 name
。使用了 this 指针可以使得代码更加清晰,尤其是在成员变量和参数名称相同或相似的情况下。
需要注意的是,在大多数情况下,this 指针的使用是隐式的,编译器会自动处理。只有在需要区分成员变量和函数参数,或者需要在成员函数中显式引用当前对象时,才需要显式地使用 this 指针。