类和对象(中1)
一. 构造函数
• 第一:我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求。• 第二:编译器默认生成的函数不满足我们的需求,我们需要自己实现,那么如何自己实现
构造函数时特殊的成员函数,它的作用是对象实例化时初始化对象,构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数自动调用的特点就完美的替代的了Init。(说明:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的原生数据类型, 如:int/char/double/指针等,自定义类型就是我们使用class/struct等关键字自己定义的类型。)
构造函数的特点:
1. 函数名与类名相同 。2. 无返回值 。 (返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)3. 对象实例化时系统会自动调用对应的构造函数。4. 构造函数可以重载。5. 如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。6. 无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数,都叫做默认构造函数。但是这 三个函数有且只有一个存在,不能同时存在 。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认生成那个叫默认构造, 实际上无参构造函数、全缺省构造函数也是默认构造,总结一下就是不传实参就可以调用的构造就叫默认构造 。7. 我们不写,编译器默认生成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要初始化列表才能解决,初始化列表,我们下个章节再细细讲解。
我给大家用代码详细的讲解关于构造函数的问题,大家在我的讲解下可能会更加明白一些,下面这段代码是我写的三个构造函数:
class Date
{
public:// 1.无参构造函数Date(){_year = 1;_month = 1;_day = 1;}// 2.带参构造函数Date(int year, int month, int day){_year = year;_month = month;_day = day;}//3.全缺省构造函数Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Print(){cout << _year << "/" << _month << "/" << _day << endl;}
private:int _year;int _month;int _day;};
#include <iostream>
#include <cstdlib> // 包含 malloc 和 free 的头文件
typedef int STDataType;
class Stack
{
public:Stack(int n = 4)//构造函数{_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");throw std::bad_alloc(); // 抛出异常}_capacity = n;_top = 0;}~Stack()//析构函数{free(_a); // 释放动态分配的内存_a = nullptr; // 避免悬空指针_capacity = 0;_top = 0;}private:STDataType* _a;size_t _capacity;size_t _top;
};
// 两个Stack实现队列
class MyQueue
{
public:// 入队操作void push(STDataType value){pushst; // 这里需要实现具体的入栈逻辑}// 出队操作STDataType pop(){// 这里需要实现具体的出栈逻辑return 0;}// 判断队列是否为空bool empty(){// 这里需要实现具体的判断逻辑return false;}private:Stack pushst;Stack popst;
};
int main()
{try{MyQueue mq;// 可以在这里调用 mq 的入队、出队等操作}catch (const std::bad_alloc& e){std::cerr << "内存分配失败: " << e.what() << std::endl;}return 0;
}
二. 析构函数
析构函数的特点:
1. 析构函数名是在类名前加上字符 ~ 。2. 无参数无返回值。 (这里跟构造类似,也不需要加void)3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。4. 对象生命周期结束时,系统会自动调用析构函数。5. 跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定类 型成员会调用他的析构函数。6. 还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说 自定义类型成员无论什么情况都会自动调用析构函数。7. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,如 Date;如果默认生成的析构就可以用,也就不需要显示写析构,如MyQueue; 但是有资申请时,一定要自己写析构,否则会造成资源泄漏,如Stack 。8. ⼀个局部域的多个对象,C++规定后定义的先析构。
只说概念大家可能也没有很直观的感受,下面我给大家举出一个简单的例子,并对比C++和C让大家更直接的感受到,我们先来看C++代码:
class DynamicArray {
private:int* arr;int size;
public:// 构造函数DynamicArray(int s) {size = s;arr = (int*)std::malloc(size * sizeof(int));std::cout << "Dynamic array of size " << size << " is created." << std::endl;}// 析构函数~DynamicArray() {std::free(arr);std::cout << "Dynamic array is destroyed." << std::endl;}
};
int main() {{// 创建对象DynamicArray da(10);} // 对象离开作用域,析构函数自动调用return 0;
}
在这个例子中,DynamicArray 类的构造函数使用 std::malloc 动态分配了内存。析构函数 ~DynamicArray() 在对象 da 离开其作用域时自动调用,使用 std::free 释放了之前分配的内存,避免了内存泄漏。下面我们对比C语言:
typedef struct {int* arr;int size;
} DynamicArray;
// 初始化函数
void initDynamicArray(DynamicArray* da, int s) {da->size = s;da->arr = (int*)malloc(s * sizeof(int));printf("Dynamic array of size %d is created.\n", da->size);
}
// 释放资源函数
void freeDynamicArray(DynamicArray* da) {free(da->arr);printf("Dynamic array is destroyed.\n");
}
int main()
{DynamicArray da;initDynamicArray(&da, 10);// 手动调用释放资源函数freeDynamicArray(&da);return 0;
}
在C语言中没有构造函数和析构函数的概念,所以需要我们手动的实现函数并且调用,需要显式调用 freeDynamicArray 函数来释放动态分配的内存,而在 C++ 中,析构函数会在对象销毁时自动调用,减少了忘记释放资源的风险,提高了代码的安全性。
三. 拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数就叫做拷贝构造函数,也就是说拷贝构造函数是一种特殊的构造函数。拷贝构造用于创建一个新对象,该新对象是另一个已有对象的副本,它的作用是在初始化对象的时候,将一个已存在对象的所有成员变量的值复制到新创建的对象中。
1. 拷贝构造函数是构造函数的一个重载。2. 拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器直接报错,因为语法逻辑上会引发无穷递归调用。 拷贝构造函数也可以多个参数,但是第一个参数必须是类类型对象的引用,后面的参数必须有缺省值 。3. C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。4. 若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造。5. 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显示实现拷贝构造。像Stack这样的类,虽然也都是内置类型, 但是_a指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝 。像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的拷贝构造会调用Stack的拷贝构造,也不需要我们显示实现MyQueue的拷贝构造。 这里还有一个小技巧,如果一个类显示实现了析构并释放资源,那么他就 需要显示写拷贝构造,否则就不需要 。6. 传值返回会产生一个临时对象调用拷贝构造,传引用返回,返回的是返回对象的别名(引用),没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于一个野引用,类似一个野指针一样。传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后还在,才能用引用返回。
拷贝构造函数的语法:
#include<iostream>
using namespace std;
//拷贝构造函数 C++中拷贝构造函数的一般形式如下:
class ClassName
{
public:ClassName(const ClassName& other){//复制成员变量this->member = other.member;}
private:int member;
};
调用拷贝构造函数的时机:
1. 对象初始化的时候:当一个已存在对象初始化另一个新对象时,会调用拷贝构造函数。
//比如说当我们已经初始化好l1的值 int main() {ClassName l1;l1.Print();ClassName l2(l1);//调用拷贝构造函数l2.Print();//相当于是已经存在的对象去初始化一个新对象,调用拷贝构造函数,上面的是把l1的值复制给l2,other就是l1的别名。return 0; }
2. 函数参数传递:当对象作为参数按值传递给函数的时候,会调用拷贝构造函数。
void function(ClassName obj) {//函数体 } ClassName obj; function(obj);//调用拷贝构造函数
3. 函数返回值:当函数返回一个对象时,会调用拷贝构造函数。
ClassName function() {ClassName obj;//进行一些操作return obj;//调用拷贝构造函数 }
上面就是调用拷贝构造函数的几种常见的场景,那么接下来我们来看它的第四个特点,关于浅拷贝和深拷贝,对于我们的日期类的类类型,其实是不需要深拷贝的,它并没有释放资源,所以不需要另外开辟新的空间,那么对于我们的栈来说,就需要深拷贝了。
class Stack
{
public:Stack(int n = 4)//构造函数{_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");throw std::bad_alloc(); // 抛出异常}_capacity = n;_top = 0;}~Stack()//析构函数{free(_a); // 释放动态分配的内存_a = nullptr; // 避免悬空指针_capacity = 0;_top = 0;}private:STDataType* _a;size_t _capacity;size_t _top;
};
int main()
{Stack str1(10);Stack str2(str1);return 0;
}
这时候就要进行深拷贝了,深拷贝就意味着我要把你的资源全部重新拷贝一份,你开辟多大的空间,那么我也开辟多大的空间,需要我们在类类型Stack中自己写的一份拷贝构造函数:
//str2(str1)
Stack(const Stack& st)//深拷贝
{_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);if (nullptr == _a){perror("malloc申请空间失败");throw std::bad_alloc(); // 抛出异常} //拷贝空间memcpy(_a, st._a, sizeof(STDataType) * st._top);//以及拷贝空间上面的值_top = st._top;_capacity = st._capacity;
}