目录
编辑
默认成员函数:
构造函数
构造函数的特性:
析构函数:
拷贝构造函数:
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。
默认生成的拷贝构造:
1.内置类型完成值拷贝
2. 若未显式定义,编译器会生成默认的拷贝构造函数。
3.拷贝构造函数的典型引用场景
默认赋值运算符
c++运算符重载
赋值:赋值运算符的重载:
默认成员函数:
对于上篇文章中讲解中所使用的类,一般正常使用是没有问题的,但是如果我们在使用某些类的时候,,比如我们的日期类使用时,忘记或者没有进行初始化会发生什么样的问题呢:
现象是编译通过,但是成员变量都是随机值。 严重一点可能会崩溃。
为了解决这个问题,C++中引入了构造函数:
构造函数
是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证 每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
构造函数的特性:
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任 务并不是开空间创建对象,而是初始化对象。 其特征如下:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦 用户显式定义编译器将不再生成。
两个默认构造函数的功能和Init函数的功能是一模一样的,但是构造函数可以默认调用:
如何调用带参数的构造函数呢:
在创建对象的时候自动调用,
没有参数调用的时候不可以在对象名字后面加上括号
因为无法和函数声明区分开。
编写某认构造的时候可以使用缺省参数,和无参调用构成重载但是不可以同时存在,无参调用时二者会产生歧义:
Date(int year = 2025, int mouth = 1, int day = 1)
{_year = year;_mouth = mouth;_day = day;
}
//写一个构造函数的重载://Date()
//{
// _year = 2025;
// _mouth = 1;
// _day = 1;
//}
通过上面的例子我们知道,如果我们不写构造函数,使用的时候也可以打印出值,不过是随机值,这就意味着在c++中如果我们不写构造函数应该是有构造函数的,接下来我们观察一个栈的实现类:
现象:队列没写构造函数但是调用了栈的构造函数,如果写一个int size正常应该是不初始化化,初始化的原因是编译器自己做了一些优化。所以这个行为是不确定的。
C语言将数据类型分为了内置类型和自定义类型,
内置类型:int char 指针等,所以Date* p是内置类型,因为是指针,指针的本质就是地址。
自定义类型:struct class enum等等。
某认生成构造函数:内置类型成员不做处理,自定义类型会去调用他的默认构造。
vs2022现象,当一个对象的成员变量中有自定义类型和内置类型的时候,都会去初始化,但是当只有内置类型的时候又不初始化,这是由于编译器的优化导致。
上述对于两种不同类型数据的区别对待使得学习c++这门语言有些冗余,于是在C++11中做了这样的补丁,对于成员变量允许这样操作:
但是,值得注意的是这里的几个成员变量在1这里依旧是声明,并没有因为初始化而变成定义
原因: 这在类中相当于还是图纸,只有当用这个类的类型实例化一个对象的时候才会开辟空间,这里的值理解成缺省值。编译器生成某认构造的时候就可以用了。
重点:无参的构造函数和全缺省的构造函数以及编译器自己生成的构造函数,都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为 是默认构造函数。也就是不传参的都是默认构造函数。三个函数不能同时存在,否则有歧义。
总结:
1.一般情况下,我们都要自己写构造函数
2.成员都是自定义类型,或者声明时给了缺省值,可以考虑让编译器自己去生成构造函数。
析构函数:
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由 编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
特性 析构函数是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构 函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
日期类实际上没有需要时释放的资源,可以看一下调用析构函数的过程:
~Date()
{cout << "data destory" << endl;
}
出了作用领域,,自动调用。
对于栈的释放资源,析构函数是必要的,内存泄漏不会报错。
~Stack()
{free(_a);_capacity = _top = 0;_a = nullptr;
}
对象的开辟和销毁是由系统来进行的
编译器 生成的默认析构函数,对自定类型成员调用它的析构函数。对于栈类,我们不写析构函数,不会自己销毁,因为编译器不敢。对内置类型不做处理,对自定义类型调用其析构函数。
拷贝构造函数:
传参本质就是一个拷贝。
对于日期类的传参拷贝:
void Test(Date d)
{d.Print();
}int main()
{Date d1;Test(d1);
这是值拷贝也就是浅拷贝
对于栈类的传参如果也使用值拷贝:
可能会出现对一块空间进行多次释放的问题。
c++规定,自定义类型的对象拷贝的时候,调用一个函数,这个函数叫做拷贝构造。
Date d2(d1) 使用d1初始化d2也是拷贝构造。
1. 拷贝构造函数是构造函数的一个重载形式。
日期类的拷贝构造:
Date(Date& d)
{_year =d._year;_mouth = d._mouth;_day = d._day;
}
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错, 因为会引发无穷递归调用。
调用拷贝构造如果是传值传参的话又会引起拷贝构造。
所以会用传指针和传引用的方式。
c++规定自定义类型对象传参拷贝,必须经过拷贝构造。可以这么理解
但是引用传参和指针传参不会发生拷贝构造函数的调用。
关于栈类的拷贝,浅拷贝可能会引起析构的时候一块空间释放两次的问题,所以栈类一般定义为深拷贝:
Stack(Stack& stt)
{_a = (int*)malloc(sizeof(int) * stt._capacity);if (_a == nullptr){perror("malloc fail");exit(-1);}memcpy(_a, stt._a, sizeof(int) * stt._top);_top = stt._top;_capacity = stt._capacity;
}
对于队列类,我们没有写拷贝构造函数,自己调用了拷贝构造:
默认生成的拷贝构造:
1.内置类型完成值拷贝
自定义类型会调用这个成员的拷贝构造。(比如栈这种类型需要我们自己写一个深拷贝,浅拷贝可能会出问题)
写拷贝构造的时候最好带上const修饰,否则会出现反向拷贝的情况。
2. 若未显式定义,编译器会生成默认的拷贝构造函数。
默认的拷贝构造函数对象按内存存储按 字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定 义类型是调用其拷贝构造函数完成拷贝的。
3.拷贝构造函数的典型引用场景
①使用已经存在的对象创建新对象
②函数参数类型为类 类型对象
③函数返回值类型为类类型对象
默认赋值运算符
对于“=”,也就是赋值运算符的运算,如果我们不显示实现编译器也会生成一个默认的。跟拷贝构造的行为类似,内置类型完成值拷贝,自定义类型调用它的赋值。stake类涉及空间开辟的情况,需要自己实现完成深层次拷贝,介绍赋值运算符之前,我们先要了解c++中的运算符重载。
c++运算符重载
对于+ - * /这样运算符内置类型可以直接使用,但是自定义类型不可以直接使用
int a = 1;
int b = 2;
c = a+ b;
但是 Date d1 Date d2
d1 == d2这样就不可以直接使用。
对于内置类型来说,编译器知道其类型的含义就可以进行比较,直接就转换成指令了
但是对于自定义类型来说,编译器不知道要按照什么样的规则来进行比较。
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其 返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
不能通过连接其他符号来创建新的操作符:比如operator@
重载操作符必须有一个类类型参数
用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐 藏的this
那么日期类的比较就可以这样来写,关于日期的比较不像自定义类型那些谁大就大,而是有一个比较逻辑,编译器是不能确定我们自己定义的类的比较逻辑的,无法统一所以就无法比较。
bool operator>(const Date& d1, const Date& d2)
{if (d1._year > d2._year){return true;}if (d1._year == d2._year && d1._mouth > d2._mouth){return true;}if (d1._year == d2._year && d1._mouth == d2._mouth && d1._day > d2._day){return true;}return false;}
此时的c++可以支持这样写;
此时的使用就和我们的内置类型使用运算符感觉没有差异了。
当我们将这个函数的实现放在类里面就会报错:
因为还有一个隐藏的this指针,参数要和操作数匹配,所以这样修改:
bool operator==( const Date& d2)
{return _year == d2._year&& _mouth == d2._mouth&& _day == d2._day;
}
bool operator>( const Date& d2)
{if (_year > d2._year){return true;}if (_year == d2._year && _mouth > d2._mouth){return true;}if (_year == d2._year && _mouth == d2._mouth && _day > d2._day){return true;}return false;}
调用时这样调用:
d1.operator>(d2);
可以重载那些运算符,主要看这个运算有没有意义,有意义就可以实现,没有意义就不要实现。
赋值:赋值运算符的重载:
两个已经存在的对象进行拷贝。而拷贝构造是一个已经存在的对象去拷贝初始化另外一个对象。
Date& operator=(const Date&d)
{if (this != &d){_year = d._year;_mouth = d._mouth;_day = d._day;}return *this;}
赋值函数是一个默认构造函数,operate= 我们不写编译器会默认生成一个。行为跟拷贝构造类型,对内置类型完成值拷贝,对自定义类型完成他的赋值。date可以不写,myqueque1不写,栈要写,完成深度拷贝。
以上就是c++中的默认成员函数以及支持的默认行为。下一篇文章将介绍c++中的运算符重载