本文从解析类的基本组成结构开始,逐步剖析类的概念
目录
一、类的基本组成结构
-
类名: 类的名称用于识别该特定类型。例如,在
class MyClass {}
中,“MyClass”就是类名。 -
数据成员: 数据成员也称为字段,是存储在类中的变量。它们可以是任何数据类型,包括基本类型(如int、float)、指针、数组、其他类的对象等。
-
成员函数: 成员函数也称为方法,是在类内部定义的函数,用于操作类的数据成员或执行与类相关的任务。
-
构造函数: 构造函数是一种特殊的成员函数,它的名字与类名相同,并没有返回类型。构造函数在创建类的对象时自动调用,用于初始化对象的状态。
-
析构函数: 析构函数也是一种特殊的成员函数,其名称为类名前加波浪线(~)。当一个对象被销毁时,析构函数自动调用,通常用于释放对象占用的资源。
-
访问修饰符: C++类中的成员有三种访问级别:public(公共)、protected(受保护)和private(私有)。这些关键字控制了类的成员对外部的可访问性。
-
静态成员: 静态成员(变量或函数)属于整个类而不是单个对象,因此所有对象共享同一个静态成员的实例。
-
友元: 友元可以是函数或另一个类,允许它们访问类的私有和受保护成员。
-
继承: 类可以从另一个类派生,这称为继承。派生类(子类)可以继承基类(父类)的特性和行为。
-
虚函数: 虚函数支持多态性,允许派生类重写基类的成员函数
二、类名
1.class的作用
在计算机编程语言中,尤其是面向对象编程(OOP)的语言如C++、Java、C#等,class
是一个基础的概念和关键字,用于定义一个新的数据类型。class
声明了一个类,类是一种用户自定义的抽象数据类型,它封装了数据(称为成员变量或属性)和对这些数据的操作(称为成员函数或方法)。
2.类名的命名规则
-
首字母大写: 传统上,C++类名的第一个字母大写。例如,
MyClass
比myclass
更常见。 -
驼峰命名法: 如果类名由多个单词组成,那么除了第一个单词外,每个后续单词的首字母都应大写。例如,
StudentRecord
或EmployeeDetails
。 -
避免使用保留字: 不要将C++的关键字或保留字用作类名,例如
class
,new
,delete
,if
,else
等。 -
描述性: 类名应该清晰地反映类的目的或功能。例如,如果一个类用于处理文件,一个好的类名可能是
FileHandler
或FileManager
。 -
避免缩写: 尽量不要使用缩写,除非它们是广泛接受的标准(如
URL
)。缩写可能会导致混淆,特别是在大型项目中。 -
统一风格: 在一个项目中,尽量保持类名命名的一致性。如果项目已经采用了某种命名约定,新的类名应该遵循这一约定。
-
避免数字: 尽量不要在类名中使用数字,除非它们具有明确的意义并且不会引起混淆。
-
避免下划线: 在C++中,通常不建议在类名中使用下划线。下划线通常用于预处理器宏或系统保留标识符。
-
长度适中: 类名应该是描述性的,但也不要过于冗长。过长的类名可能会影响代码的可读性。
3.类的命名示例
假设命名一个汽车的类
1 class Car{
2 // 类的实现...
3 };
三、数据成员与成员函数
1.数据成员的简介
数据成员是类或结构体的一部分,用于存储类或结构体实例(对象)的状态或属性。在面向对象编程中,数据成员可以被视为对象的变量,它们保存着对象的特定信息。每个对象都有其数据成员的独立副本,这意味着即使是在同一类的不同对象之间,数据成员也可以持有不同的值。
数据成员可以是任何数据类型,包括但不限于整型、浮点型、字符型、布尔型、指针、数组、字符串,甚至是其他类的对象。它们可以被声明为public
、private
或protected
,这决定了数据成员的访问级别和可见性。
例如,在C++中,一个表示人的类可以有如下的数据成员:
class Person {
private:
std::string name; // 私有数据成员,表示人的名字
int age; // 私有数据成员,表示人的年龄
std::string address; // 私有数据成员,表示人的住址
public:
// 公共成员函数,用于访问和操作私有数据成员
void setName(const std::string& newName);
std::string getName() const;
void setAge(int newAge);
int getAge() const;
void setAddress(const std::string& newAddress);
std::string getAddress() const;
};
在这个例子中,name
、age
和address
是Person
类的私有数据成员,它们存储了个人的具体信息。为了保护这些数据成员,避免直接访问和修改,它们被声明为private
。然后,通过提供公共的成员函数(例如setName
、getName
等),类的使用者可以间接地访问和修改这些数据成员,这增强了数据的安全性和封装性。
数据成员是面向对象编程中实现封装、继承和多态的基础之一,它们帮助构建了类的内部状态,而类的外部则通过成员函数与这些状态进行交互。
2.成员函数的简介
成员函数就是在类中定义的各种用来对数据成员进行操作的一系列函数。
四、权限
1.三种主要的控制权限
-
public(公有): 当成员被声明为
public
时,它们可以从类的外部被访问,包括从其他类、函数以及程序的其他部分。这意味着public
成员对于类的使用者是完全开放的,可以被自由地读取和修改。 -
private(私有):
private
成员只能被类的成员函数访问,包括友元函数和类的友元类的成员函数。private
成员对外部世界是不可见的,这有助于封装和隐藏类的内部实现细节,从而增强代码的安全性和可维护性。 -
protected(受保护):
protected
成员的行为介于public
和private
之间。protected
成员可以被类的成员函数访问,并且也可以被派生类的成员函数访问。但是,它们不能被类的外部代码直接访问。这种访问权限特别适用于基类的设计,使得派生类可以继承和扩展基类的功能,同时又不让这些成员对整个程序公开。
2、三种控制权限的访问示例
1.public访问示例
示例中展示了,通过成员函数访问和直接外部访问两种方法
#include <iostream>
// 定义一个公开成员的类
class MyClass {
public:
int publicData; // 公开数据成员
// 公开成员函数
void displayPublicData() const {
std::cout << "Public data is: " << publicData << std::endl;
}
};
// 主函数
int main() {
MyClass obj; // 创建一个MyClass类型的对象
// 直接访问公开数据成员
obj.publicData = 10;
std::cout << "Accessing public data directly: " << obj.publicData << std::endl;
// 调用公开成员函数
obj.displayPublicData();
return 0;
}
2.private访问示例
示例展示点:
① 直接访问私有权限的数据成员,出现了错误
② 通过成员函数修改了私有的数据成员
③ 通过成员函数获取了私有数据成员的值
私有的特点:只可以由自己类内部定义的成员函数进行访问,不可以直接在外部调用使用
#include <iostream>
class MyClass {
private:
int privateData; // 私有数据成员
public:
MyClass() : privateData(10) {} // 构造函数可以直接访问privateData
void modifyPrivateData(int value) { // 成员函数可以访问privateData
privateData = value;
}
int getPrivateData() const { // 成员函数可以返回privateData的值
return privateData;
}
};
int main() {
MyClass obj;
// 试图直接访问privateData会导致编译错误
// obj.privateData = 20;
// 通过成员函数修改privateData
obj.modifyPrivateData(20);
// 通过成员函数获取privateData的值
std::cout << "Private data: " << obj.getPrivateData() << std::endl;
return 0;
}
3.protect访问示例
protect具有三种访问:
-
类自身的成员函数:
protected
成员可以被类本身的任何成员函数访问。 -
派生类的成员函数: 当一个类从另一个类派生时,基类的
protected
成员会成为派生类的protected
成员。这意味着派生类的成员函数可以访问基类的protected
成员,就像它们是派生类自己的成员一样。 -
派生类的友元函数或友元类: 派生类的友元函数或友元类也可以访问基类的
protected
成员,这为实现一些需要访问派生类和基类内部细节的算法提供了灵活性
示例:
#include <iostream>
// 基类
class Base {
protected:
int protectedData; // 受保护的数据成员
public:
Base() : protectedData(10) {}
void displayProtectedData() const {
std::cout << "Protected data from Base class: " << protectedData << std::endl;
}
};
// 派生类
class Derived : public Base {
public:
void accessProtected() {
protectedData = 20; // 可以访问基类的protectedData
displayProtectedData(); // 可以调用基类的protected成员函数
}
};
// 主函数
int main() {
Derived d;
d.accessProtected(); // 调用派生类的函数,该函数可以访问基类的protected成员
return 0;
}
构造函数可以和析构函数结合起来看,两者作用相反
五、构造函数
1、构造函数的简介
① 特殊的成员函数
② 每次创建对象时自动调用
③ 主要作用:设置初始状态获执行必要的准备工作,确保对象处于有效和一致的状态。
2、构造函数的特点
① 名称与类名相同:构造函数的名称必须与其所属的类名完全相同。
② 无返回类型:构造函数没有返回类型,甚至不使用void
作为返回类型。
③ 自动调用:每当使用类创建一个新对象时,构造函数会被自动调用。
④ 可以有参数:构造函数可以有零个或多个参数,这些参数用于传递给对象的初始值。
⑤ 可以重载:在一个类中可以定义多个构造函数,只要它们的参数列表不同,这就是构造函数重载。这允许以不同的方式初始化对象。
⑥ 初始化列表:构造函数可以使用初始化列表来初始化数据成员,这通常比在函数体内赋值更高效,尤其是在初始化引用或常量成员时。
3.构造函数的类型
-
默认构造函数:没有参数的构造函数,用于创建对象而无需任何初始值。
-
参数化构造函数:带有参数的构造函数,用于初始化对象时传入初始值。
-
拷贝构造函数:这是一种特殊的构造函数,用于创建一个对象作为另一个对象的副本。它接受一个同类型的对象引用作为参数。
-
移动构造函数:类似于拷贝构造函数,但用于移动语义,通常用于从右值引用构造对象,以提高性能。
4、构造函数示例
#include <iostream>
#include <string>
class Person {
private:
std::string name;
int age;
public:
// 默认构造函数
Person() : name("Unknown"), age(0) {
std::cout << "Default constructor called." << std::endl;
}
// 参数化构造函数
Person(std::string n, int a) : name(n), age(a) {
std::cout << "Parameterized constructor called." << std::endl;
}
// 拷贝构造函数
Person(const Person& other) : name(other.name), age(other.age) {
std::cout << "Copy constructor called." << std::endl;
}
// 移动构造函数
Person(Person&& other) noexcept : name(std::move(other.name)), age(other.age) {
std::cout << "Move constructor called." << std::endl;
}
// 其他成员函数...
};
int main() {
// 使用默认构造函数创建对象
Person p1;
// 使用参数化构造函数创建对象
Person p2("Alice", 30);
// 使用拷贝构造函数创建对象
Person p3(p2);
// 使用移动构造函数创建对象
Person p4 = std::move(p3);
return 0;
}
六、析构函数
1.构造函数的特点
-
名称:析构函数的名称是在类名前加上波浪线
~
,如~ClassName()
。 -
无参数:析构函数不能有参数,也不接受任何形式的参数。
-
无返回类型:析构函数没有返回类型,甚至不使用
void
作为返回类型。 -
自动调用:当对象的作用域结束或者使用
delete
删除对象时,析构函数会被自动调用。 -
单个析构函数:一个类只能有一个析构函数,这意味着析构函数不能被重载。
-
调用顺序:当一个对象包含其他对象作为成员时,析构函数的调用顺序是先调用成员对象的析构函数,再调用自身类的析构函数。这个顺序保证了先清理派生类的资源,后清理基类的资源。
2.构造函数的用途
-
释放资源:析构函数最常用于释放对象在构造时分配的资源,如动态分配的内存、文件句柄、网络连接等。
-
执行清理:执行任何必要的清理操作,如关闭打开的文件、停止线程、取消注册的事件监听器等。
3.构造函数示例
#include <iostream>
class MyClass {
private:
int* data; // 动态分配的内存
public:
MyClass() {
data = new int(10);
std::cout << "Object created." << std::endl;
}
~MyClass() {
delete data;
std::cout << "Object destroyed and resources released." << std::endl;
}
};
int main() {
{
MyClass obj; // 创建对象,构造函数被调用
} // 作用域结束,析构函数被调用
return 0;
}
在这个示例中,MyClass
的析构函数负责释放由构造函数动态分配的内存。当obj
的作用域结束时,析构函数自动调用,确保data
指向的内存被正确释放,防止内存泄漏。
七、静态成员
在C++中,类的静态成员包括静态数据成员和静态成员函数。这些成员与类本身相关联,而不是与类的任何特定对象实例相关联。
1.静态数据成员
静态数据成员被所有对象共享。这意味着无论创建多少个对象,静态数据成员只有一个副本。它们在类外部定义,并且可以通过类名直接访问,而无需创建对象。
定义与初始化: 静态数据成员必须在类外初始化,因为它们是全局变量,需要分配内存。例如:
1 class MyClass {
2 public:
3 static int count; // 声明
4 };
5
6 // 在类外部初始化
7 int MyClass::count = 0;
访问: 静态数据成员可以通过类名直接访问,或者通过类的对象指针或引用间接访问:
1 MyClass::count++; // 直接访问
2 MyClass obj;
3 obj.count++; // 间接访问,但这通常被视为不好的做法
2.静态成员函数
静态成员函数也是与类相关联的,而不是与任何特定对象关联。它们可以访问类的静态数据成员,但不能访问非静态数据成员。静态成员函数没有this
指针。
定义与调用: 静态成员函数可以在类内部或外部定义。它们可以通过类名直接调用,而无需创建类的对象。
1 class MyClass {
2 public:
3 static void func(); // 声明
4 };
5
6 void MyClass::func() { // 定义
7 std::cout << "Hello from static function" << std::endl;
8 }
9
10 int main() {
11 MyClass::func(); // 调用
12 return 0;
13 }
总结:
- 静态数据成员在类的所有对象间共享同一份数据。
- 静态成员函数可以访问静态数据成员,但不能访问非静态数据成员。
- 静态数据成员和静态成员函数都可以通过类名直接访问,无需实例化类的对象。
- 静态数据成员必须在类外初始化,静态成员函数可以在类内或类外定义。
八、友元
“友元”(Friend)是一个重要的概念,它允许非成员函数或者另一个类的成员函数访问某个类的私有(private)和受保护(protected)成员。友元关系是由该类显式声明的,使用friend
关键字来指定。
1.友元的种类
① 友元函数(Friend Function):
这是一个普通的函数,但被一个类声明为友元后,它可以访问这个类的所有私有和受保护成员。友元函数的声明通常出现在类的公共部分。
class MyClass {
public:
friend void myFriendFunction(MyClass&); // 声明友元函数
private:
int secret;
};
void myFriendFunction(MyClass& obj) {
obj.secret = 42; // 友元函数可以访问私有成员
}
② 友元类(Friend Class):
一个类可以被声明为另一个类的友元,这意味着整个类的所有成员函数都可以访问另一个类的私有和受保护成员。
class MyClass {};
class MyFriendClass {
public:
void accessPrivateMembersOfMyClass(MyClass& obj);
};
class MyClass {
public:
friend class MyFriendClass; // 声明MyFriendClass为友元类
private:
int secret;
};
void MyFriendClass::accessPrivateMembersOfMyClass(MyClass& obj) {
obj.secret = 42; // 友元类的成员可以访问私有成员
}
③ 友元成员函数
一个类的成员函数被另一个类声明为友元,从而能够访问那个类的私有或受保护成员。这种机制在某些设计模式中非常有用,尤其是在需要精细控制访问权限的情况下,比如在观察者模式、策略模式等中。
下面是一个简单的例子,展示如何使用友元成员函数:
假设我们有一个Account
类,用于表示银行账户。我们希望限制对账户余额的直接访问,但允许特定的管理功能,如审计或账户调整,访问账户的详细信息。为此,我们可以创建一个AuditManager
类,其成员函数可以作为Account
类的友元,以访问账户的私有成员。
#include <iostream>
class Account {
private:
double balance;
public:
Account(double initialBalance) : balance(initialBalance) {}
// 允许AuditManager的成员函数访问balance
friend void AuditManager::checkBalance(const Account& account);
void deposit(double amount) {
balance += amount;
}
void withdraw(double amount) {
if (amount <= balance)
balance -= amount;
else
std::cout << "Insufficient funds." << std::endl;
}
};
class AuditManager {
public:
void checkBalance(const Account& account) {
std::cout << "Account Balance: " << account.balance << std::endl;
}
};
int main() {
Account myAccount(1000.0);
myAccount.deposit(500.0);
AuditManager manager;
manager.checkBalance(myAccount); // 由于是友元,可以直接访问balance
return 0;
}
在这个例子中,AuditManager::checkBalance
函数是Account
类的友元,因此它可以访问Account
的私有成员balance
。这使得AuditManager
能够执行审计任务,同时Account
类的封装性得到了保持,因为普通用户无法直接访问balance
。
九、继承
继承是面向对象编程(OOP)中的一个核心概念,它允许在一个类(称为子类或派生类)中继承另一个类(称为父类或基类)的属性和方法。通过继承,子类可以重用和扩展父类的功能,而无需重复编写代码。这不仅减少了代码量,提高了代码的可维护性和可读性,还促进了代码的复用和模块化。
继承的一些关键点:
-
属性和方法的继承: 子类自动获得父类的所有非私有属性和方法。这意味着子类可以使用这些属性和方法,就好像它们是在子类中定义的一样。
-
多态性: 继承支持多态性,即一个接口可以由多个类实现。在运行时,可以通过父类的引用或指针来调用子类的方法,这样就可以根据实际对象类型动态选择具体的方法实现。
-
覆盖与隐藏: 子类可以覆盖(override)父类的方法,以提供不同的实现。如果子类中定义了一个与父类同名的属性或方法,那么在子类中会隐藏父类的同名成员。
-
构造函数和析构函数: 构造函数和析构函数不会被继承,但子类可以调用父类的构造函数来初始化继承的成员。析构函数通常应该在基类中声明为虚函数,以便正确地清理派生类的对象。
-
访问修饰符: 继承有三种访问模式:公有(public)、保护(protected)和私有(private)。默认情况下,C++中的继承是私有的。
下面是一个使用C++编写的继承示例:
#include <iostream>
// 基类
class Animal {
public:
Animal(std::string name) : m_name(name) {}
virtual void speak() const { std::cout << "The animal makes a sound." << std::endl; }
virtual ~Animal() {}
protected:
std::string m_name;
};
// 派生类
class Dog : public Animal {
public:
Dog(std::string name) : Animal(name) {}
void speak() const override { std::cout << m_name << " says Woof!" << std::endl; }
};
int main() {
Animal *animal = new Dog("Rex");
animal->speak(); // 输出: Rex says Woof!
delete animal;
return 0;
}
2.派生类的定义
派生类(也被称为子类或衍生类)是从一个或多个现有类(基类或父类)继承而来的类。派生类可以重用和扩展基类的特性和行为。定义派生类时,你可以选择继承基类的不同访问级别(public, protected, 或 private),这决定了哪些成员可以从派生类中访问。
定义派生类的基本语法:
class Derived : access_specifier Base {
// 派生类的成员
};
其中:
Derived
是派生类的名字。Base
是基类的名字。access_specifier
是访问限定符,可以是public
,protected
, 或private
,它决定了基类的成员在派生类中的访问性。
访问限定符的作用:
-
Public 继承: 当你使用
public
关键字进行继承时,基类中的public
和protected
成员在派生类中保持相同的访问级别,而private
成员则不可访问。
假设我们有以下基类 BaseClass:
class BaseClass {
public:
void publicMethod();
protected:
void protectedMethod();
private:
void privateMethod();
};
现在我们定义一个派生类 DerivedClass,使用 public 继承:
class DerivedClass : public BaseClass {
public:
void callMethods() {
publicMethod(); // 可以访问
protectedMethod(); // 可以访问
// privateMethod(); // 不可访问,会引发编译错误
}
};
-
Protected 继承: 使用
protected
关键字进行继承时,基类中的public
和protected
成员在派生类中变为protected
,而private
成员仍然不可访问。
#include <iostream>
// 基类
class Base {
public:
void publicMethod() { std::cout << "Base Public Method\n"; }
protected:
void protectedMethod() { std::cout << "Base Protected Method\n"; }
private:
void privateMethod() { std::cout << "Base Private Method\n"; }
};
// 派生类,使用保护继承
class Derived : protected Base {
public:
// 派生类的公共方法,可以调用基类的protected方法
void derivedMethod() {
publicMethod(); // 仍为protected,在派生类外部不可见
protectedMethod(); // 可以访问
// privateMethod(); // 不可访问,会引发编译错误
}
};
// 再次派生的类
class DerivedDerived : public Derived {
public:
void derivedDerivedMethod() {
// 由于protectedMethod在Derived中是protected的,
// DerivedDerived可以访问它
protectedMethod();
}
};
int main() {
Derived d;
DerivedDerived dd;
// 下面的代码将产生编译错误,因为基类的public和protected成员在派生类中变成了protected
// d.publicMethod();
// d.derivedMethod(); // 也会失败,因为derivedMethod现在是protected的
// 下面的代码可以成功编译,因为DerivedDerived继承了Derived的protected成员
dd.derivedDerivedMethod();
return 0;
}
-
Private 继承: 使用
private
关键字进行继承时,基类中的所有成员在派生类中都变成private
,并且对于派生类的子类也不可访问。
#include <iostream>
// 基类
class Base {
public:
void publicMethod() { std::cout << "Base Public Method\n"; }
protected:
void protectedMethod() { std::cout << "Base Protected Method\n"; }
private:
void privateMethod() { std::cout << "Base Private Method\n"; }
};
// 派生类,使用私有继承
class Derived : private Base {
public:
// 派生类的公共方法,可以调用基类的public和protected方法
void derivedMethod() {
publicMethod(); // 可以访问
protectedMethod(); // 也可以访问
// privateMethod(); // 不可访问,会引发编译错误
}
};
int main() {
Derived d;
d.derivedMethod(); // 调用派生类的公共方法
// 下面的代码将产生编译错误,因为基类的public和protected成员在派生类中变成了private
// d.publicMethod();
return 0;
}
十、虚函数
虚函数(Virtual Function)是C++中用于实现多态性的关键特性之一。在面向对象编程中,多态性允许我们使用基类类型的指针或引用来调用派生类中重写的方法,从而实现基于对象类型的动态绑定。虚函数使得在运行时能够确定调用哪个具体的函数实现,而不仅仅依赖于变量或指针的类型。
声明虚函数
要声明一个虚函数,你需要在函数前加上virtual
关键字。这告诉编译器该函数是一个虚函数,可能在派生类中被重写。虚函数可以是成员函数,也可以是析构函数。
1 class Base {
2 public:
3 virtual void print() const { /* ... */ } // 虚函数声明
4 virtual ~Base() {} // 虚析构函数
5 };
6
7 class Derived : public Base {
8 public:
9 void print() const override { /* ... */ } // 重写虚函数
10 };
虚析构函数
当一个类包含虚函数时,通常建议也声明虚析构函数。这是因为在多态上下文中,你可能使用基类指针删除派生类对象。如果没有虚析构函数,只会调用基类的析构函数,这可能导致资源泄漏或未定义行为。
1 class Base {
2 public:
3 virtual ~Base() {} // 虚析构函数
4 };
5
6 class Derived : public Base {
7 public:
8 ~Derived() { /* ... */ } // 正常析构
9 };
动态绑定
虚函数的关键在于动态绑定,即函数调用的实际版本取决于对象的实际类型,而不是指针或引用的类型。这在运行时决定,因此称为动态绑定。
1 #include <iostream>
2
3 class Base {
4 public:
5 virtual void sayHello() const {
6 std::cout << "Hello from Base\n";
7 }
8 };
9
10 class Derived : public Base {
11 public:
12 void sayHello() const override {
13 std::cout << "Hello from Derived\n";
14 }
15 };
16
17 int main() {
18 Base* basePtr = new Derived(); // 指向Derived的Base指针
19 basePtr->sayHello(); // 输出: Hello from Derived
20 delete basePtr;
21 return 0;
22}
注意事项
- 虚函数表:每个含有虚函数的类都会有一个虚函数表(vtable),用于存储指向类中所有虚函数的指针。这意味着虚函数调用会有额外的开销,因为需要查找虚函数表。
- 性能影响:虚函数调用通常比非虚函数慢,因为需要额外的间接寻址。然而,现代编译器优化技术,如内联和链接时优化,可以减少这种性能差距。
override
和final
:C++11引入了override
和final
关键字。override
用于确保派生类中的函数确实重写了基类中的虚函数,final
用于防止函数在派生类中被进一步重写。
虚函数和多态性是面向对象编程的重要组成部分,它们使代码更加灵活和可扩展。正确使用虚函数可以帮助你构建出更加健壮和可维护的软件系统。
未完待续...........持续更新