构造函数
在学习C语言栈等数据结构知识时候,我们都必须先对栈初始化然后再进行一系列增删的操作最后还要防止内存泄漏进行free非常的麻烦。
这就有了C++中的构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时即实例化对象 由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
虽说构造函数名称为构造但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
class Stack
{
public:
Stack()//函数名和类名相同 无返回值不是void而是无
{
_a = nullptr;
_size = _capacity = 0;
}
//并且支持重载
Stack(int n )
{
_a = (int*)malloc(sizeof(int) * n);
if (_a == nullptr)
{
perror("malloc申请空间失败");
exit(-1);
}
_capacity = n;
_size = 0;
}
void Push(int x)
{}
void Destroy()
{}
//。。。。。
private:
int* _a;
int _size;
int _capacity;
};
int main()
{
Stack st1;//无参不需要加()
Stack st2(3);//带参数的则需要(参数)
return 0;
}
理解上面的例子再结合构造函数的特性:
1 函数名与类名相同。( 定义了构造函数Stack()和Stack(int n ))
2 无返回值。 (这里的无返回值不是指void而是没有)
3 对象实例化时编译器自动调用对应的构造函数。(对象实例化的同时自动调用构造函数,也一定会调用构造函数,所以构造函数的定义不能出错,下面有例子)
4 构造函数可以重载。(支持重载也代表着他有多个构造函数就有多种初始化方式)
这样就可以不用再去先写Init对栈先进行初始化再进行插入和删除,写了构造函数那么初始化就由编译器自动调用
特别要注意的是对象实例化时编译器自动调用对应的构造函数而如果类中没有定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
理解下面的错误:是错误
特别注意无参的构造函数和全缺省的构造函数不能同时存在只能存在一个理解下面例子
//是错误的错误的
class Date
{
public:
Date()
{
year = 1;
month = 1;
day = 1;
}
Date(int year=1,int month=1,int day=1)
{
_year = year;
_month = month;
_day = day
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
return 0;
}
当实例化了Date d 无传参时编译器就不知道该调用哪一个,因此会形成歧义
还要理解一个知识点:默认构造函数不仅仅只代表编译器自动生成的构造函数,其中无参的构造函数和全缺省的构造函数都称为默认构造函数并且默认构造函数只能有一个。
那么编译器自动生成的默认构造函数又有什么作用呢?
得分成两种情况看首先C++把类型分成内置类型(基本类型)和自定义类型。
内置类型就是int/char/double等
自定义类型就是我们使用class/struct/union等自己定义的类型
例子
class Time
{public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
调试
可以看到编译器自动生成的构造函数并不会对内置类型成员有作用,内置类型成员依旧是随机数,而对于自定义类型成员会自动调用的它的默认成员函数。(对内置类型成员不起作用,对自定义类型成员起作用)
对于内置类型来讲当自己没有写构造函数,而编译器生成的默认构造函数又对内置类型成员不起作用这合理吗?当然不合理所以在C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
调试:
不再是随机值
上图也符合缺省函数的特性
析构函数
析构函数可以和构造函数一起记忆,在类对象实例化时会调用构造函数进行初始化而当类对象生命周期结束时会调用析构函数用于完成对象中资源的清理工作(好比在用C语言写栈时不需要在手动调用destroy的函数用来free malloc申请的空间)这里还需要理解 析构函数不是完成对类对象本身的销毁,类对象本身的销毁是由编译器完成的,析构函数只是负责当对象销毁时完成对资源的清理工作
析构函数的特性:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。(无参无返)
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
class Stack
{
public:
Stack()
{
_a = (int*)malloc(sizeof(int) * 4);
_size = 0;
_capacity = 4;
}
~Stack()
{
free(_a);
_a = nullptr;
_size = 0;
_capacity = 0;
}
private:
int* _a;
int _size;
int _capacity;
};
class ZJK
{
public:
ZJK()
{
_aa = 1;
_bb = 2;
_cc = 3;
}
private:
int _aa;
int _bb;
int _cc;
Stack s;
};
int main()
{
ZJK s;
return 0;
}
调试可得到
和构造函数一样当类中没有析构函数时编译器会自动生成默认的析构函数,而默认生成的析构函数也不会对内置类型处理,只会对自定义类型处理即自动调用该自定义类型的析构函数
如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数而有资源申请时,一定要写,否则会造成资源泄漏,比如上面的Stack类。
拷贝构造函数
为什么会有拷贝构造函数?
这里就要讲到编译器的特性,编译器能力只能做浅拷贝(值拷贝)就好比memcpy一个一个字节进行拷贝所以对于内置类型之间的拷贝编译器能够处理,但对于对于申请了空间的成员则必须要用到手动写拷贝构造函数来实现深拷贝。
class Stack
{
public:
Stack()
{
_a = (int*)malloc(sizeof(int) * 4);
_size = 0;
_capacity = 4;
}
~Stack()
{
free(_a);
_a = nullptr;
_size = 0;
_capacity = 0;
}
private:
int* _a;
int _size;
int _capacity;
};
int main()
{
Stack A;
Stack B(A);
return 0;
}
这块理解就是编译器默认用浅拷贝把Stack A拷贝给Stack B但是最后结果报错了
从下面调试图中理解为什么会报错
实现了拷贝 但是 B对象中成员变量_a和A对象的成员变量_a相同这有什么影响?
影响一:
首先要明白对象A和对象B开辟了两个栈而对于不同的栈来说必须要相互不影响,而若是对Stack A中push一些数据A的size和capacity会改变,但是对于StackB来说他也指向了malloc开辟的空间,空间里存储的值已经改变但是他的size和capacity并不会改变,当对Stack B push一些数据时就会对原有的值进行覆盖
影响二:
当对象的生命周期结束时,编译器会自动调用默认的析构函数来对资源进行清理,编译器会先清理B对象再清理A对象(栈帧栈帧就是后进先出),这就会导致野指针的情况导致程序报错,B对象的指向的_a的空间已经被释放且被置空,但A对象_a的值已经指向原来的地址当对其进行释放时就引发报错了;
这也是C语言的缺点所以对于C语言C++增加了拷贝构造函数
拷贝构造函数的特点:
1 拷贝构造函数是特殊的构造函数且参数只有一个且必须是类类型对象的引用
2 当我们没有写拷贝构造函数时编译器会自动生成默认的拷贝构造函数但也就是上面说的编译器的能力只能实现浅拷贝,但与构造函数和析构函数不同对于内置类型拷贝构造函数进行浅拷贝对于自定义类型则是调用他的拷贝构造函数。(内置函数自定义函数都起作用)
3 类中若申请了资源则必须自己写拷贝构造函数实行深拷贝,相反则不需要自己实现,编译器的浅拷贝足以(和析构函数一起记忆,有资源申请析构函数要写拷贝构造函数也要写没有资源申请则都可以不写)
4 拷贝构造函数典型的调用场景
1 使用已存在对象创建新对象 2 函数参数类型为类类型对象 3 函数返回值类型为类类型对象
最开始Stack类中拷贝构造函数的正确写法
class Stack
{
public:
Stack()//构造函数
{
_a = (int*)malloc(sizeof(int) * 4);
_size = 0;
_capacity = 4;
}
~Stack()//析构函数
{
free(_a);
_a = nullptr;
_size = 0;
_capacity = 0;
}
Stack(Stack& t)//拷贝构造函数
{
_a= (int*)malloc(sizeof(int) * t._capacity);
_size = t._size;
_capacity = t._capacity;
}
private:
int* _a;
int _size;
int _capacity;
};
int main()
{
Stack A;
Stack B(A);//Stack B = A;
return 0;
}
注意 Stack B(A);和Stack B = A;都可以这两种写法都是拷贝构造
那么特点1 中为什么必须是类类型对象的引用?传值传参会造成什么后果。
这里要结合特点4 一起理解 使用已存在对象创建新对象时会拷贝构造的调用,如Stack B(A)调用Stack(Stack& t),这里又涉及到了传参的知识Stack& t类型是引用所以传参的方式是传引用传参无需拷贝 t 就是对象A的别名,但如果拷贝函数是Stack(Stack t)为传值传参要把stack A 拷贝给stack t 又根据
特点4中 函数参数类型为类类型对象则需要调用拷贝函数(也可以这么理解我们刚开始的目的是把 stack A 赋值给 stack B 所以调用了拷贝构造函数来实现深拷贝,但如果拷贝构造函数里的参数是类型是类对象,则调用拷贝构造函数时我们先要做的就是把 stack A 的值赋值给 stack t 那么就和stack A 赋值给 stack B 一样 又要去调用拷贝构造函数 反反复复)
后果是无穷递归调用拷贝构造函数所以参数只有一个且必须是类类型对象的引用
赋值运算符重载
学习赋值运算符重载前要写学习运算符重载
运算符重载
为什么会有运算符重载?
例子:
如果只是为了实现int类型的a和b的加法,你肯定不会这么写,因为你知道int类型的a和b可以直接相加。
但实际上 为什么通过 + 这个运算符可以实现a和b的相加?这当人都是由编译器设计好的,使得代码又简单又能让人理解。
但是如果是两个自定义类型的对象还能这样相加吗?当然不能。
所以C++设计了运算符重载,使得编写的代码增加了可读性。
运算符重载特点:
1 运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数一样。
2 关键字operator后面接需要重载的运算符符号
注意:不能通过连接其他符号来创建新的操作符:比如operator@
对于改变原有的内置类型间的操作符如 1+1 =2 不能再重载 + 。,这也意味着重载操作符必须有一个类类型参数
例子 :判断是否为同一天
class Date
{
public:
Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
public://注意点 用运算符重载时全局的所以不能成员变量不能为private
int _year;
int _month;
int _day;
};
bool operator==(const Date& A1, const Date& B1)
{
return A1._year == B1._year
&& A1._month == B1._month
&& A1._day == B1._day;
}
int main()
{
Date A(2023, 3, 6);
Date B(2023, 9, 1);
cout << (A == B) << endl;//注意要加()因为操作符的优先级不同
cout << operator ==(A,B)<< endl;//不同的写法
return 0;
}
参数和操作数是对应的如果有两个参数那么第一个参数就是左操作数 第二个参数就是右操作数。
如果不把原先私有的成员变量改成公有,那么可以把运算符重载写进Date类中,这里就要注意隐藏的this参数
class Date
{
public:
Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& B1)//注意this隐藏
{
return _year == B1._year
&& _month == B1._month
&& _day == B1._day;
}
private://改变从public 改成private
int _year;
int _month;
int _day;
};
int main()
{
Date A(2023, 3, 6);
Date B(2023, 9, 1);
cout << (A == B) << endl;
cout << A.operator ==(B) << endl;//两种写法都可以
return 0;
}
理解了运算符重载那么赋值运算符重载就很好理解了。
例子:
class Date
{
public:
Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date& operator=(const Date& B1)
{
_year = B1._year;
_month = B1._month;
_day = B1._day;
return *this;
}
void print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
public:
int _year;
int _month;
int _day;
};
int main()
{
Date A(2023, 3, 6);
Date B(2023, 9, 1);
Date C(2023, 2, 1);
C = A = B;
A.print();
C.print();
return 0;
}
运行
而当我们不写赋值运算符重载时,编译器也会自动生成默认的赋值运算符重载,和拷贝构造函数相似对内置类型和自定义类型都起作用,重点就是是否为深拷贝,深拷贝则必须写赋值运算符重载浅拷贝则不需要。
上个例子的Date类则可以不写赋值运算符重载
class Date
{
public:
Date(int year = 2000, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
public:
int _year;
int _month;
int _day;
};
int main()
{
Date A(2023, 3, 6);
Date B(2023, 9, 1);
Date C(2023, 2, 1);
C = A = B;
A.print();
C.print();
return 0;
}
运行结果不变
注意理清楚下面
不是赋值而是拷贝构造,注意区别 赋值重载是两个已经实例化好了的对象,而Date B =A 则是用对象A初始化B,别忘了拷贝构造函数是一个特殊的构造函数(构造函数作用就是对象实例化的同时进行初始化)
拷贝对象时的一些编译器优化
理解:
class ZJK
{
public:
ZJK()
{
cout << "构造函数" << endl;
}
ZJK(const ZJK& a)
{
cout << "拷贝构造函数" << endl;
}
~ZJK()
{
cout << "析构函数" << endl;
}
void operator=(const ZJK& a)
{
cout << "赋值重载" << endl;
}
};
ZJK func()
{
ZJK A;
return A;
}
int main()
{
ZJK a = func();
cout <<" --------------------------- "<< endl;
ZJK b; b = func();
return 0;
}
ZJK a = func();的调试结果
ZJK b; b = func();的调试结果
这上面就是编译器做到优化
总结出来的结论:接收返回值对象,尽量拷贝构造方式接收不要赋值去接收