C++:类和对象(中)
前面已经初步认识了C++的类和对象。简单来说,类相当于一个壳子,将成员函数和成员变量封装起来。如果你觉得C++中的类不过如此,只是把C语言中的结构体多加了一个成员函数,那你就大错特错了。本篇文章,我们将学习类中的默认成员函数。
目录
一、类的默认成员函数
1.1、默认成员函数的概念
1.2、默认成员函数的分类
二、构造函数
2.1、构造函数的概念(干什么用的?)
2.2、构造函数的原型(语法形式)
2.3、构造函数的注意事项(怎么用)
2.4、默认构造函数
三、析构函数
3.1、析构函数的概念
3.2、析构函数的语法形式
3.3、注意事项
四、拷贝构造函数
4.1、拷贝构造函数的基本概念
4.2、基本语法
4.3、注意事项
五、赋值运算符重载
5.1、运算符重载
5.2、赋值运算符重载
一、类的默认成员函数
1.1、默认成员函数的概念
在类中,如果你没有显示定义某些成员函数(就是你没有写出来的某些函数),编译器会自动生成相关的函数,成为默认成员函数。
1.2、默认成员函数的分类
共有六种:构造函数、析构函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值预算符。(最后两个函数为C++11引入的)
二、构造函数
2.1、构造函数的概念(干什么用的?)
构造函数是特殊的成员函数,其作用是对象实例化时初始化对象。说人话,构造函数就是用来给你用类创建的对象初始化用的。
2.2、构造函数的原型(语法形式)
classname( 参数 )
{//…………
}
注意:构造函数的名字不能随便取,构造函数的名字必须是类的名字!!!
2.3、构造函数的注意事项(怎么用)
(1)构造函数的名字必须是类的名字,重要的事情要多说几遍!
(2)构造函数无返回值,连前面的void也不能写。因为不返回返回值也是一种返回值;
(3)构造函数可以重载;
(4)对象实例化时,系统会自动调用对应的构造函数;
下面举个例子:
using namespace std;
class student
{
private:string _name;int _number;int _age;
public:student(){_name = "张三";_number = 2023001;_age = 18;}//构造函数重载student(string name, int number, int age){_name = name;_number = number;_age = age;}//打印学生信息void StudentPrint(){cout << "学生姓名:" << _name << endl;cout << "学生学号:" << _number << endl;cout << "学生年龄:" << _age << endl;}
};
int main()
{student s1;student s2("李四" ,2023005 ,19);s1.StudentPrint();s2.StudentPrint();return 0;
}
注意,在你用类创建对象(实例化)的时候,编译器会自动调用构造函数,所以,构造函数的传参直接在对象后面加个括号传。比如:student s2("李四" ,2023005 ,19);
(5)只有你自己不写构造函数的时候,编译器才会替你写,一旦你自己编写构造函数,编译器就不再生成构造函数了;
(6)对于编译器自己生成的构造函数,是有缺陷的:对内置类型成员变量的初始化没有要求。换句话说,是否初始化时不确定的,主要看编译器的种类。
2.4、默认构造函数
把编译器自己生成的构造函数,我们自己写的无参构造函数,全缺省构造函数统一称为默认构造函数。主包这里概括一下,就是不用你传参构造函数也能运行的就是默认构造函数。
这三个默认构造函数同时只能出现一个,不能同时存在。
三、析构函数
3.1、析构函数的概念
析构函数也是一个特殊的成员函数,当对象声明周期结束时,自动执行必要的清理工作。主要是清理对象在生命周期内申请的内存等资源,防止泄露。
3.2、析构函数的语法形式
~classname()
{//…………
}
析构函数的名字是类的名字前加上一个~ ,必须是这样!!!
3.3、注意事项
(1)析构函数的名字必须是 ~ 加上类的名字;
(2)析构函数没有返回值,void也不能写;
(3)析构函数不能接收参数;
(4)析构函数在一个类中只能有一个,不能重载;
(5)对象生命周期结束时,系统会自动调用析构函数;
(6)对于系生成的默认构造函数,对内置类型不做处理;
(7)如果类中没有申请资源,那么析构函数可以不写,直接让编译器自己生成好了;
(8)一个局部域的多个对象,C++规定后定义的先析构。
下面通过栈来演示一下析构函数:
typedef int StackDataType;
class Stack
{
private:StackDataType* _arr;int _top;int _capacity;
public:Stack(){_arr = nullptr;_top = _capacity = 0;}//入栈void StackPush(StackDataType x){if (_top == _capacity){int newCapacity = _capacity == 0 ? 4 : 2 * _capacity;StackDataType* tmd = (StackDataType*)realloc(_arr, sizeof(StackDataType) * newCapacity);if (tmd == nullptr){perror("realloc fail!");exit(1);}_arr = tmd;_capacity = newCapacity;}_arr[_top++] = x;}//析构函数~Stack(){free(_arr);_arr = nullptr;_top = _capacity = 0;}
};
四、拷贝构造函数
4.1、拷贝构造函数的基本概念
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫拷贝构造函数。说人话,拷贝构造函数是用一个已存在的对象(拷贝源)来初始化另一个对象(拷贝对象)。
4.2、基本语法
classname( classname& other)
{//…………
}
拷贝构造函数的名字必须是类的名字,且第一个参数必须是类的引用!!!
4.3、注意事项
(1)拷贝构造函数是构造函数的重载;
(2)拷贝构造函数的第一个参数必须是当前类类型对象的引用,后面的参数必须有缺省值;
(3)C++规定自定义类型对象进行拷贝行为必须调用拷贝构造函数,所以自定义类型的传值传参和传值返回都会调用拷贝构造函数;
(4)若为定义显示拷贝构造函数,编译器会自己生成一个拷贝构造函数;
(5)如果一个类显式实现了析构并释放了资源,那么他就需要显式拷贝构造函数,否则就不需要;
(6)传值返回会产生一个临时对象的拷贝构造,传值引用返回,返回的是对象的别名,没有产生拷贝。
下面举个例子(以栈为例):
Stack(const Stack& st)
{_arr = (StackDataType*)malloc(sizeof(StackDataType) * st._capacity);if (_arr == nullptr){perror("malloc fail!");exit(1);}_top = st._top;_capacity = st._capacity;memcpy(_arr, st._arr, sizeof(StackDataType) * _top);
}
调用拷贝构造函数的用法也很简单:
Stack s1;
Stack s2 = s1;
Stack s3(s1); //两种方式均可以
五、赋值运算符重载
5.1、运算符重载
主包这里不想copy这个冗杂不好理解的定义了,主包要自己定义。运算符重载就是赋予C++运算符(如 +, -, =, ==, [] 等)操作自定义类型(类或结构体)的能力,让它们用起来和内置类型(如 int, double)一样直观。
什么意思?比如一开始我们创建的Student这个类,你拿两个学生相减 s1 - s2 算怎么回事?编译器无法理解,这时候你可以通过特殊手段赋予减号一定的含义,比如:两个学生的成绩之差。
class Student
{
private:string _name;int _number;int _age;float score;
public:float operator(Student& s1){return _score - s1._score;}
};
下面有几个注意事项:
(1)运算符重载是一个具有特殊名字的函数,它的名字由 operator 和后面的运算符组成;
(2)如果一个重载运算符是类的成员函数,则它的第一个运算对象默认传给 this 指针,所以在类里面的重载运算符定义时可以少写一个参数;
(3)运算符重载不改变它的优先级和结合性;
(4) .* :: sizeof ?: . 这五个运算符不能重载;
(5)重载运算符至少要给一个类型的参数;
(6)++ 有前置 ++a和 后置 a++,为了区分这两个的重构,C++规定,后置++需要增加一个int形参。
int operator++()
{cout << " ++a" << endl;return _age+1;
}
//后置++,需要跟一个int形参
int operator++(int)
{cout << " a++" << endl;return _age+1;
}
5.2、赋值运算符重载
主包老毛病又犯了,不想看那些冗长且复杂的定义了,主包要自己定义。赋值运算符重载 (operator=
) 让你能像内置类型(如 int
)一样,用等号 (=
) 将一个对象的值“拷贝”给另一个已存在的同类型对象。
赋值运算重载符是一个默认成员函数,用于完成两个已经存在的对象的拷贝赋值。
student& operator=(const student& s1)
{if (this != &s1){_age = s1._age;_name = s1._name;_number = s1._number;_score = s1._score;}return *this;
}
注意,这里加一个 if 条件是为了处理有些人闲的让自己给自己赋值。返回*this 的引用是为了处理连续赋值的情况,s1 = s2 = s3 ,因为如果不返回,那么s2的值就无法得到,返回引用是为了避免再次创建临时空间调用拷贝构造函数,提高效率。
注意事项:
(1)赋值运算符重载规定必须重载为成员函数;
(2)有返回值,推荐写成当前类类型引用,提高效率;
(3)没有显示实现赋值运算符重载时,编译器会自己生成一个默认赋值运算符重载,对内置类型成员变量,对内置类型成员变量完成值拷贝(浅拷贝)。