引言:
本来打算用一篇介绍清楚C++中的类与对象,再三考虑后觉得不妥:第一,知识点实在太多;第二,对于从刚学完C并打算过渡到C++的朋友来说,学的太深较有难度…
总而言之,我打算用三到四篇文章去介绍,难度由易到难,希望可以帮助各位能全面且无压力的学习该内容
本文也会不断的更新,比如加入一些图片或者动图来帮助大家理解,如有好想法希望各位能够多多批评指正
第一部分:类的基本概念
类的定义
在C++中,类是一种用户自定义的数据类型(类似于C语言中的struct
),它将数据(属性
)和操作这些数据的函数(方法
)封装在一起。
类是面向对象编程的核心概念之一,它允许我们以一种结构化的方式组织代码,提高代码的可重用性和可维护性。
定义一个类
类的定义通常包含以下几个部分:
- 类名:类的名字应该具有描述性,以便其他开发者能够理解该类的用途。
- 成员变量:类内部的数据,也称为属性。
- 成员函数:类内部的函数,用于操作成员变量。
对比C语言来看,"内容"不过多了一个成员函数(但是大有讲头)
下面是一个简单的类定义示例:
#include <iostream>
using namespace std;class Person {// 成员变量string name;int age;public:// 成员函数void setName(string n) {name = n;}void setAge(int a) {age = a;}void display() {cout << "Name: " << name << ", Age: " << age << endl;}
};int main() {Person p;p.setName("Alice");p.setAge(30);p.display();return 0;
}
在这个例子中:依旧是经典的Person类
Person
是类名。name
和age
是成员变量。setName
、setAge
和display
是成员函数。
访问控制
C++ 中提供了三种访问控制关键字:public
、private
和 protected
。这些关键字用于控制类成员的可见性和可访问性。
- public:公共成员,可以在类的外部直接访问。
- private:私有成员,只能在类的内部访问。
- protected:受保护成员,只能在类的内部和派生类中访问。
访问控制示例
#include <iostream>
using namespace std;class Person {
private:string _name;int _age;public:void setName(string n) {_name = n;}void setAge(int a) {_age = a;}void display() {cout << "Name: " << _name << ", Age: " << _age << endl;}
};int main() {Person p;p.setName("李四");p.setAge(18);p.display();// 下面这行代码会编译错误,因为 _name 是私有的// p._name = "王五";return 0;
}
在这个例子中,_name
和 _age
被声明为 private
,因此不能在类的外部直接访问它们。只有通过 setName
和 setAge
这样的公共成员函数才能修改这些私有成员。
💡tips:建议各位在声明私有变量时在前方加入符号’_’
类域
类定义了一个作用域,类内的所有成员都在类的作用域;若在类外定义成员函数,需加入::
此时将不再是内联函数(inline
,详细见历史文章)
类影响的是编译的规则,如果不加person::
,则默认全局查找,加入就会告知编译profession
函数是person类的成员函数
void person::profession()
{cout << "类外定义"<<endl;
}
类的实例化
可理解为图纸 -> 实物(class_type -> obj
)的过程
person p1;
用person实例化出一个对象,person充当图纸,实物充当实物。
类对象的大小
在文章开头提到 C++ 的
class
和 C的struct
类似,各位也能感受出来。
class
中成员变量也是需要对齐的,法则与结构体一致
各位可以注意到,class
中有成员函数的存在,也需要对齐吗,如果这样每个对象都需要开出这么多空间吗?我们可以创建两个变量,然后对比两者成员函数的地址
显而易见,两对象的成员函数地址一致,该类的成员函数共用一块内存
构造函数与析构函数
构造函数和析构函数是特殊的成员函数,它们分别在对象创建和销毁时自动调用。
名称 | 作用 |
---|---|
构造函数 | 用于初始化对象。构造函数的名称与类名相同,且没有返回值类型。 |
析构函数 | 用于清理对象占用的资源。析构函数的名称与类名相同,但在前面加上波浪线 ~ ,且没有参数和返回值类型 |
声明
- 在类名前加
~
- 无参数无返回值
- 一个类只能有一个析构函数,若未显式定义,则由系统默认生成
- 对象生命周期结束时,编译器自动调用析构函数
- 跟构造函数相似,对内置类型不做处理,自定义类型会调用其析构函数
- 我们显示编写析构函数,自定义类型被销毁时一样会调用析构函数
~person(){delete[] _name;free(this->ps);ps = nullptr;}
如果该类有其他类的对象会怎样呢?
#include <iostream>
using namespace std;class Test {
private:int socre;
public:Test(int score=0){}~Test() {cout << "Test析构函数" << endl;}
};class Person {
private:string _name;int _age;Test _t1;public:void setName(string n) {_name = n;}void setAge(int a) {_age = a;}void display() {cout << "Name: " << _name << ", Age: " << _age << endl;}~Person() {cout << "Person类析构函数" << endl;}
};int main() {Person p;return 0;
}
Person类析构函数
Test析构函数
运行结果如上,在 C++ 中,对象的析构顺序与其构造顺序相反。具体来说:
构造顺序 | 先构造 Person 对象 p。在构造 Person 对象时,会构造其成员对象 _t1 |
析构顺序 | 当 Person 对象 p 的生命周期结束时,先调用 Person 类的析构函数 ~Person()。在 Person 类的析构函数中,会销毁其成员对象 _t1,因此调用 Test 类的析构函数 ~Test()。 |
因此,Person
类的析构函数先调用,然后才是 Test
类的析构函数。这是因为 Person
对象的整体生命周期结束后,才会销毁其成员对象。
拷贝构造函数
-
拷贝构造函数是构造函数的一种重载。
-
拷贝构造函数的参数必须是类型对象的引用,传值方式会导致无穷递归调用。
-
自定义类型对象进行拷贝时必须调用拷贝构造函数,因此传值传参和传值返回都会调用拷贝构造。
-
若未显式定义拷贝构造函数,编译器会生成一个默认的拷贝构造函数
-
对内置类型成员变量进行浅拷贝,对自定义类型成员变量调用其拷贝构造函数。
-
如果一个类显式实现了析构函数并释放资源,那么也需要显式实现拷贝构造函数。
-
传值返回会产生临时对象并调用拷贝构造函数,而传引用返回不会产生拷贝,但要确保返回的对象在函数结束后仍然有效,否则会返回野引用。传引用可以减少不必要的拷贝,但要确保引用的有效性。
对于第二条各位可能不太好理解,假如我们这么定义
//以下为错误示范,请勿模仿
Person(Person source) { _name = source._name; _age = source._age;
}
//.....
int main(){Person p1("张三","18");Person p2(p1);
}
由于是传值操作,相当于把p1
拷贝了一份并且该变量也是个Person
类对象,也需要借助拷贝构造进行拷贝,因此会造成无限递归,十分危险
正确示范
class Person {
private:string _name;int _age;public:// 默认构造函数Person(_name = nullptr,_age = 0) :{}// 拷贝构造函数Person(const Person& other) {strcpy(_name,name);_age = age;}void setName(string n) {_name = n;}void setAge(int a) {_age = a;}void display() {cout << "Name: " << name << ", Age: " << age << endl;}
};int main() {Person p1;p1.setName("张三");p1.setAge(18);Person p2(p1); // 调用拷贝构造函数p2.display();return 0;
}
在这个例子中,_name
是一个动态分配的字符串指针。析构函数负责释放 _name
指向的内存,防止内存泄漏。
默认析构函数不会进行delete (free) 操作,需要手动释放内存