C++类和对象
Posted 学IT的小卢
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++类和对象相关的知识,希望对你有一定的参考价值。
类和对象(三)
拷贝构造函数:
当我们想要将一个已确定的类变量的值拷贝给另外一个相同类型的类变量,有什么快捷的方法吗?
就相当于定义了一个int类型的i=10,想将i复制给一个刚初始化的遍历j,int j=i;
这里我们就可以用到拷贝构造函数,以下我们来解释一下拷贝构造函数!!!
拷贝构造函数的写法:
class 类名
public:
类名(const 类名& 变量名)
;
拷贝构造函数的特征:
- 拷贝构造函数是一种特殊的构造函数,是构造函数的重载
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,
因为会引发无穷递归调用。
拷贝构造函数如何定义?拷贝构造函数的参数如何设置?:
拷贝构造函数的参数直接传类的变量?传值调用?
这里报错了?为什么呢,直接函数传值 为什么会报错呢?
这里编译器编译不过去的,因为编译器认为这里会无限递归,编译器的警告是不允许参数的类型是类
这是为什么呢?我们先来理解一个例子,再回来看它!
此时我们有两个类,一个是日期类,应该是栈类,除了栈类中的a是指向动态开辟的空间,其他都是int类型
class date //日期类 public: int year; int month; int day; ; class stack //栈类 public: int* a; int top; int capacity; ;
当我们为这两个类写其的拷贝构造函数用的是传值调用的情况下:
日期类是没有问题,因为编译器会为内置类型拷贝遍历创建临时变量
而栈类中top和capacity都是内置类型没有问题,主要是a这个变量,a是一个指向开辟在堆区的数组
假设st1和st2都是由st这个对象通过拷贝构造函数(传值调用)拷贝出来的,拷贝的时候会连指向空间的那个指针一起拷贝出来,也就是拷贝出来会变成st1和st2两个变量的a都指向同一块空间,这样会造成很严重的错误(进栈和free的时候)
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请
时,则拷贝构造函数是一定要写的,否则就是浅拷贝。在C++中,函数调用如果是传值调用都是需要创建一个临时变量,将值拷贝一份给这个临时变量,而将值拷贝一份本身就是一个拷贝构造函数,因此就会死循环,造成无穷递归。
若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
浅拷贝就是编译器自己会完成的,就一个内置类型按字节大小拷贝变量
字节序拷贝会如果变量是一个指向开辟在堆区的数组的指针,拷贝的不只有那个数组,还有其中的数据和指向这个数组的指针
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定
义类型是调用其拷贝构造函数完成拷贝的。
编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
我们最好在参数加个const修饰,这样可以防止权限放大
Date(const Date& d)
_year = d._year;
_month = d._month;
_day = d._day;
int main()
Date d1;//如果这里加个Const修饰,而上面参数没有用const修饰
Date d2(d1);
权限只能缩小不能放大
当d1被const修饰后,就不能可以被修改,当参数没有const的时候,权限就会放大,就会造成错误。
运算符重载:
运算符重载的语法:
函数原型:返回值类型 operator操作符(参数列表)
运算符重载的注意点:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
- .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
bool operator==(const Date& d)
//日期类
return _year == d._year &&
_month == d._month &&
_day == d._day;
<<(流插入)的优先级比==高,因此下面这个语句会报错
cout << d1 == d2 << endl;
赋值运算符重载
赋值运算符重载格式
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要复合连续赋值的含义
//赋值运算符的重载
Date& operator=(const Date &d)//这里可以不需要用&,不会无穷递归
if (this != &d)
//解决自己给自己赋值的问题
_year = d._year;
_month = d._month;
_day = d._day;
//解决连续赋值的问题
return *this;
赋值运算符只能重载成类的成员函数不能重载成全局函数
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
if (&left != &right)
left._year = right._year;
left._month = right._month;
left._day = right._day;
return left;
// 编译失败:
// error C2801: “operator =”必须是非静态成员
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现
一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值
运算符重载只能是类的成员函数。
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符
重载完成赋值。
前置++和后置++的运算符重载:
前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
Date& Date::operator++()
//++d1
*this += 1;
return *this;
Date Date::operator++(int)
//d1++
Date tmp(*this);
*this += 1;
return tmp;
前置++:返回+1之后的结果
注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存
const成员函数:
class A
public:
void Print()const
cout << _a << endl;
private:
int _a = 10;
;
int main()
const A aa;//加const会报错,权限扩大了
aa.Print();//*this
return 0;
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数
隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
请思考下面的几个问题:
- const对象可以调用非const成员函数吗?
- 非const对象可以调用const成员函数吗?
- const成员函数内可以调用其它的非const成员函数吗?
- 非const成员函数内可以调用其它的const成员函数吗?
1.不可以
2.可以
3.不可以
4.可以
注意:权限只能缩小,不能扩大
取地址及const取地址操作符重载:
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需
要重载,比如想让别人获取到指定的内容!
class A
public:
void Print()const
cout << _a << endl;
A* operator&()
return this;
const A* operator&()const
return this;
private:
int _a = 10;
;
int main()
const A aa;//加const会报错,权限扩大了
aa.Print();//*this
cout << &aa << endl;
return 0;
日期类:
Date.h文件:
#pragma once
#pragma once
#include<iostream>
#include<cassert>
using namespace std;
class Date
friend istream& operator>>(istream& in, Date& d);
friend ostream& operator<<(ostream& out,const Date&d);
public:
Date(int year = 1900, int month = 1, int day = 1);
void Print()const;
int GetMonthDay(int year, int month)const;
bool operator==(const Date& d)const;
bool operator!=(const Date& d)const;
bool operator<(const Date& d)const;
bool operator<=(const Date& d)const;
bool operator>(const Date& d)const;
bool operator>=(const Date& d)const;
Date& operator+=(int day);
Date operator+(int day)const;
Date& operator-=(int day);
Date operator-(int day) const;
int operator-( const Date& d) ;
Date& operator++();//++d1
Date operator++(int);//d1++
Date& operator--();//--d1
Date operator--(int);//d1--
private:
int _year;
int _month;
int _day;
;
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);
Date.cpp文件:
#pragma once
#include"Date.h"
int Date::GetMonthDay(int year, int month)const
assert(month > 0 && month < 13);
int monthArray[13] = 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ;
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400) == 0))
return 29;
else
return monthArray[month];
Date::Date(int year, int month, int day)
//_year = year;
//_month = month;
//_day = day;
//检查是否合法
if (month > 0 && month < 13 && day <= GetMonthDay(year, month))
_year = year;
_month = month;
_day = day;
else
cout << "日期非法" << endl;
void Date::Print()const
cout << _year << '/' << _month << '/' << _day << endl;
bool Date:: operator==(const Date& d)const
//这里我们的成员是共有的,如果私有怎么解决呢?
//那我们直接放在类里面,这里报错说:参数太多,
//类里面的函数会有一个隐藏的参数将其中一个参数删了就好了
return _year == d._year &&
_month == d._month &&
_day == d._day;
bool Date::operator<(const Date& d)const
return _year < d._year
|| (_year == d._year && _month < d._month)
|| (_year == d._year && _month == d._month && _day < d._day);
Date Date::operator+(int day)const
Date tmp(*this);
tmp += day;
return tmp;
Date& Date::operator+=(int day)
if (day < 0)
*this -= -day;
return *this;
_day += day;
while (_day > GetMonthDay(_year, _month))
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
++_year;
_month = 1;
return *this;
bool Date::operator<=(const Date& d)const
return *this < d || *this == d;
bool Date::operator>(const Date& d)const
return !(*this <= d);
bool Date::operator>=(const Date& d)const
return !(*this < d);
Date& Date:: operator-=(int day)
if (day < 0)
*this += -day;
return *this;
_day -= day;
while (_day <= 0)
--_month;
if (_month == 0)
_year--;
_month = 12;
_day += GetMonthDay(_year, _month);
return *this;
Date Date:: operator-(int day)const
Date tmp(*this);
tmp -= day;
return tmp;
int Date::operator-(const Date& d)
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
max = d;
min = *this;
flag = -1;
int n = 0;
while (min != max)
++min;
++n;
return n * flag;
bool Date:: operator!=(const Date& d)const
if (*this == d)return false;
return true;
Date& Date::operator++()
//++d1
*this += 1;
return *this;
Date Date::operator++(int)
//d1++
Date tmp(*this);
*this += 1;
return tmp;
Date& Date:: operator--()//--d1
*this -= 1;
return *this;
Date Date::operator--(int)//d1--
//效率低一些
Date tmp(*this);
*this -= 1;
return tmp;
ostream& operator<<(ostream& out,const Date&d)
//全局函数不能访问私有
cout << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
istream& operator>>(istream& in, Date& d)
in >> d._year >> d._month >> d._day;
return in;
s;
Date Date::operator++(int)
//d1++
Date tmp(*this);
*this += 1;
return tmp;
Date& Date:: operator--()//--d1
*this -= 1;
return *this;
Date Date::operator--(int)//d1--
//效率低一些
Date tmp(*this);
*this -= 1;
return tmp;
ostream& operator<<(ostream& out,const Date&d)
//全局函数不能访问私有
cout << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
istreamc++ — 类和对象
目录
我们在学习C语言时知道,C语言是面向过程的语言,他关系的是是完成事情的过程,以函数进行驱动,在对于比较规模的程序来说,我们可以直接编写出一个面向过程的程序,但是对于规模较大的程序,我们就不得不重新思考办法了。
一.面向对象的程序设计
1.1对象
客观世界中,任何一个事物都可以看成是对象。
我们在这里举个栗子:一个学生它具有的属性有姓名,学号,年龄,性别;它所具有的行为有上课,吃饭,睡觉,写作业。在这里我们可知属性为静态特征,而此时的行为为动态特征。而类就是对其重新抽象。任何一个对象都应当具有属性和行为这两个要素。
int main(){
struct Student stu1; //在c中,stu1表示结构体变量
student stu2;//在c++中stu2表示对象
}
1.2面向对象的三大特性
C++是基于面向对象的程序,面向对象有三大特性即:封装、继承、多态。
特性一:封装
将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行 交互。所谓的封装就有两方面的含义,一是将数据与方法封装在一个对象中,形成一个基本的单位,各个对象至今互不影响;二是将对象中默写部分对外进行隐蔽,即就是隐蔽内部细节,只留下少量接口以便于与外部进行联系。这种对外界进行隐蔽的做法叫做信息隐蔽。
特性二:继承
继承是一个进程,通过一个进程,一个对象可以获得另一个对象的属性(数据和函数),且可以想起加入属于自己的方法和属性。用这种方法可以自动的对一个类中提供另一个类的成员和数据结构。当A类被另B类继承,A类可以称之为基类或者父类;B类被称之为派生类或者子类。
特性三:多态
如果几个相似而不完全相同的对象,当对他发出同一个消息时,他们的反映各不相同,执行不同的操作。这种现象叫做多态现象。在C++中多态是指由继承而产生的相关的不同的类,其对象会对同一消息做出不同的相应。他能增加程序的灵活性。
1.3类和对象的关系
在C++中对象的类型叫做类(class)。类描述了对象的共性和特征。类是对象的抽象,而对象是类的实例。
二.类的声明和对象的定义
2.1声明类的类型
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号。类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数。
class student{
char name[100];
int ID_number;
int age;
char gender;//类的成员变量
void learning();
void eating();
void sleeping();
void working();//类的成员函数
};
student stu1,stu2;//定义了两个student类的对象
如上现在封装在类中的stu1和stu2两个对象中的成员都对外是隐蔽的,在类外不能直接调用类的成员。但是现在缺少了对外的接口,因此我们要使用成员访问限定符public和private。private声明以下部分为私有,不可以再类的外部进行调用,而pubilc声明以下部分是公有的,可以在类外进行调用。而一般情况下,在类中的定义及不指定是private或者是public时,我们默认为时私有的。
2.2对象的定义
方法一:先声明类类型在定义对象
student stu1,stu2;//直接用类名定义了两个student类的对象
方法二:在声明类的同时定义对象
class student{
char name[100];
int ID_number;
int age;
char gender;
void learning();
void eating();
void sleeping();
void working();
}stu1,stu2;//定义了两个student类的对象
方法三:不出现类名,直接定义对象
class{
char name[100];
int ID_number;
int age;
char gender;
void learning();
void eating();
void sleeping();
void working();
}stu1,stu2;
直接定义对象在C++中是合法的,但是不常用,不建议使用。
问题:C++中struct和class的区别是什么?
解答:C++需要兼容C语言,所以C++中struct可以当成结构体去使用。另外C++中struct还可以用来定义类。 和class是定义类是一样的,区别是struct的成员默认访问方式是public,class是的成员默认访问方式是 private。
三.类的成员函数
类的成员函数是类的成员,出现在类内。他可以被定义为private,public,protected。成员函数可以访问本类中的所有成员,可以引用在本作用域中的所有数据。一般的做法是将需要被外界调用的成员函数指定为public,他们是类对外的接口。
3.1类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用 :: 作用域解析符 指明成员属于哪个类域。
void student::prints()
{
cout<<_name<<" "<<ID_number<<" "<<_gender<<" "<<_age<<endl;
}
如果函数在作用域符::的前面没有类名,或者函数名前面即无类名有无作用域符,如:
::函数名()
或
函数名()
则表示函数不适于任何类,这个函数不是成员函数,而是全局函数,即使普通函数。
3.2内置成员函数
3.2.1内置函数与宏
在C中,宏定义是十分常用的,但是又有许多缺陷。"宏并不是函数,宏并不是语句,宏并不是定义类型。"他是直接嵌入的,代码量较多,容易出错,而且无类型,无法进行类型检测,对带参的宏来说,并不会检查参数的合法性。因此在C++中提供以inline修饰的函数即内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销, 内联函数提升程序运行的效率。
内联函数的特点:
- inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。
- inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等 等,编译器优化时会忽略掉内联。
- inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
3.2.2内置成员函数
在类体中定义的成员函数中不包括循环控制结构,C++就会自动对他们处理做内联函数,这样可以大大减少调用成员函数的时间。C++要求在对一般内置函数要用关键字inline声明,但对类定义的成员函数,可以省略,因为这些成员函数已经被隐含的指定为内置函数。
class Date{
public:
...
inline void display();//声明此成员为内置函数
...
};
inline void Date::display(){//在类外定义display()函数为内置函数
cout<<"the date is"<<" year:"<<_year<<" month:"<<_month<<" day:"<<_day<<endl;
}
3.3成员函数的存储方式
同一类的不同对象中的数据成员的值一般是不相同的,而不相同对象的函数的代码是相同的,无论调用哪一个对象函数的代码,其实都是调用同样内容的代码。这样做会大大节约内存空间。
在这里如何求类的大小呢?
一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐,注意空类的大小,空类比 较特殊,编译器给了空类一个字节来唯一标识这个类。
内存对齐规则:
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的对齐数为8
- 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是 所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
举个栗子:
class{
public:
int hour;
int minute;
int sec;
void set(){
cin>>a>>b>>c;
}
};
在这里类所占的空间大小为12个字节,说明对象所占空间大小只取决于该对象中成员所占空间,而与成员函数无关。
四.this指针
在之前我们提到每个对象中的数据成员都分别占有存储空间,如果对同一个类定义了n个对象,即有n组同样大小的空间以存放n个对象中的数据成员。但是不同的对象都调用同一个函数的目标代码。在每一个成员函数中都包含一个特殊的指针,这个指针的名字是固定的,称之为this。他是指向本类对象的指针(他被作为隐藏参数传递给方法),他的值是当前被调用的成员函数所在的对象的起始地址,一般来说所有的类方法都将this指针作为条用它的对象的地址。this指针的空间在栈上,而this指针有可能为空,但是会崩溃。
例如:
int Date::get_date(){
cin>>_year;
cin>>_month;
cin>>_day;
}
会被处理为:
int Date::get_date(Date *this){
cin>>this->_year;
cin>>this->_month;
cin>>this->_day;
}
即在成员函数的形参列表中增加了一个this指针。在调用该成员函数时,实际上以t1.get_date(&t1);的方式调用的,将对象t1的地址传给形参this指针,然后按this指针的指向其引用其他成员。而这些都是编译系统自动实现的,我们不必自己动手在形参中添加this指针。
注意!!!!
每个成员函数(包括构造和析构函数)都有一个this指针,this指针指向调用对象。如果方法需要引用整个调用对象,则可使用表达式*this,在函数括号后面面使用const限定符将this进行限定,就不可以使用this来修改对象的值。然而,返回的并不是this,因为this是对象的地址,而不是对象本身,即*this(将解除引用运算符用到指针前,将得到指针指向的值)。现在可以将*this作为调用对象的别名完成前面的方法定义。
this指针的特性:
- this指针的类型:类类型* const(在C里面相当于常量引用)
- 只能在“成员函数”的内部使用
- this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this 形参。所以对象中不存储this指针。
- this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
五.默认成员函数
如果一个类中什么成员都没有,简称为空类。任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。我们将六个默认函数分成三大类:初始化和清理,拷贝复制,取地址重载。
5.1初始化和清理
5.1.1构造函数
在基于对象的程序中,在定义一个对象时,也需要对数据成员进行赋值,但是有一点特别重要,直接在声明类时对数据成员进行初始化是不正确的,因为类并非是一个实体,而是一种抽象类型,并不占存储空间。如果一个类中的所有成员都是公有的,则可以在定义对象是对数据成员进行初始化。但是如果数据成员是私有的呢,又该如何处理。这是我们就要用到C++为我们提供的构造函数来处理对象的初始化。构造函数是一中特殊的成员函数,与其他的成员函数不同,不需要用户来调用它,而是在建立对象是自动执行。
a.用构造函数实现数据成员的初始化
构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象。他的特征如下:
- 构造函数的名字必须与类名相同,而不能任意命名;
- 它不具有任何类型,不返回任何值;
- 编译器自定调用;
- 在整个对象的声明周期内只调用一次;
- 如果没有显示任何构造函数,则会生成一个默认的无参构造函数。
b.带参的构造函数
有时我们希望对不同的对象赋予不同的值,这是就无可以使用带参的构造函数,在调用不同对象的构造函数时从外面将不同的数据传递给构造函数,以实现不同的初始化。他的一般格式如下:
//构造函数首部的一般格式
构造参数名(类型1 形参1,类型2 形参2,...)
//定义对象的一般格式
类名 对象名(实参1,实参2,...);
在这里我们要注意,带参数的构造函数中的形参,其对应的实参实在建立对象时给定的。在建立对象的同时指定数据成员的初始值。 定义不同对象时使用的是参数不同的,他们反映了建立对象的属性,此方法可实现对不同对象的不同初始化。
c.用参数初始化表对数据成员初始化
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称作为类对象成员的初始化,构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内 可以多次赋值。通过赋值语句可以对数据成员进行初始化,C++还提供了另一种初始化数据成员的方法--参数初始化列表来实现对数据成员的初始化。
带有参数初始化表的构造函数一般形式:
类名::构造函数([参数列表])[:成员初始化表]{
[构造函数体]
}
注意:
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 类中包含以下成员,必须放在初始化列表位置进行初始化:(引用成员变量;const成员变量;自定义成员变量)
- 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使 用初始化列表初始化。
- 成员变量在类中声明的次序就是其在初始化列表中初始化的次序,与其在初始化列表中的先后顺序无关。
构造函数初始化时必须采用初始化列表一共有三种情况:
- 需要初始化的数据成员是对象(继承时调用基类构造函数)
- 需要初始化const修饰的类成员
- 需要初始化引用成员数据
d.构造参数重载
为了给对象不同的初始化方法,我们可以在一个类中定义多个构造函数,虽然这些构造函数名字相同但是他们的参数或者参数类型不相同,这就称为函数的重载。
我么在这里举个栗子:
在建立对象是不给出实参的构造函数,称之为默认构造函数,无参构造函数和全缺省的构造函数都是默认构造函数,但是它们两个不能同时存在,会产生二义性。 在类中没有显示任何的构造函数时,编译器会自动生成一个无参构造函数。
//无参的构造函数
Date(){}
//全缺省的构造函数
Date(int _year=1,int month=1,int day=1){
_year=year;;
_month=month;
_day=day;
}
e.explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用。 explicit关键字又来修饰构造函数,将会禁止单构造函数的隐式转换。
(链接:单参构造函数和类型转化函数)
5.1.2析构函数
它也是一个特殊的成员函数且与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。当对象生命周期结束时,会自动调用析构函数。当这个函数被调用结束时,对象应该对象应该被释放,在对象释放前自动执行析构函数。析构函数不返回任何值,没有任何函数类型,也没有函数参数。一个类可以有多个构造函数但是只能有一个析构函数,也就是说,析构函不能进行重载。如果用户没有定义析构函数,那么编译器会自动生成一个析构函数。
析构函数的特征:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值。
- 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
5.2拷贝复制
5.2.1拷贝构造函数
a.拷贝构造函数的概念
只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象 创建新对象时由编译器自动调用。
b.拷贝构造函数的特点
拷贝构造函数也是特殊的成员函数,它的特点如下:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1){
_year = year;
_month = month;
_day = day;
}
Date(const Date& d){ //拷贝构造函数
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
- 拷贝构造函数是构造函数的一个重载形式;
- 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用;
- 若未显示定义,系统生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。
Date d1(1900,1,1);
Date d2(d1);//将d1对象中的成员拷贝放置到d2中
在上面代码中,如果要对其进行析构时(此时d1与d2公用同一个空间),先销毁d2,然后释放成野指针,再一次释放d1时会崩溃。 如果一个类中未涉及到了资源管理时,拷贝函数是否提供都可以,但是涉及到了资源管理时拷贝构造函数必须提供,否则编译器生成的默认拷贝构造函数会出现问题。在底层他们共享同一份资源,这种拷贝叫做浅拷贝,浅拷贝会存在内存泄漏。(浅拷贝与深拷贝)
c.拷贝构造函数的调用时机
- 对象以值传递的方式传递函数参数
class Date{
public:
//构造函数
Date(int y){
year=y;
}
Date(const Date& d){
year=d.year;
}
private:
int year;
};
void get_year(Date d1){
cout<<"aaa"<<endl;
}
int main(){
Date d(1900);
//传入对象
get_year(d);
return 0;
}
//调用get_year()时会的步骤:d对象传入形参时,先会产生一个临时变量d1,
//然后调用拷贝构造函数将d的值给d1。等get_year()执行结束后析构掉d1对象。
- 对象以值传递的方式进行函数返回
class Date{
public:
//构造函数
Date(int y){
year=y;
}
Date(const Date& d){
year=d.year;
}
private:
int year;
};
void get_year(){
Date y(1800);
return y;
}
int main(){
get_year();
return 0;
}
//在这个过程中调用get_year()所要的过程:
//先会产生一个临时变量(X),然后调用拷贝构造函数将y的值传给这个临时变量(X)
//到函数执行到最后先析构y局部变量
//最后在get_year()执行完后在析构X对象。
- 一个对象要通过另一个对象进行初始化
5.2.2赋值重载
a.运算符重载的含义
函数重载就是对一个已有的函数赋予新的含义且可以实现新得功能。C++为了增强代码的可读性引入了运算符重载。
b.运算符重载的方法
运算符重载是通过定义函数实现的,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。运算符重载实质上就是函数重载。函数名字为:关键字operator后面接需要重载的运算符符号。 函数原型:返回值型 operator操作符(参数列表)。
他的一般格式如下:
函数类型 operator运算符名称(形参列表){
//对运算符进行重载处理
}
//如:
Date operator+(Date& d);
举个栗子,实现两个复数相加:
#include<iostream>
using namespace std;
class Complex {
public:
Complex(){//构造函数
real = 0;
imag = 0;
}
Complex(dluble r, double l){
real = r;
imag = l;
}
//声明重载运算符+
Complex operator+(Complex& c2);
void display();
private:
double real;
double imag;
}
Complex Complex::operator+(Complex& c2){
Complex c;
c.real = real + c2.real;
c.imag = imag + c2.imag;
return c;
};
void Complex::display(){
cout << "(" << real << "," << imag << ")" << endl;
}
int main(){
Complex c1(1, 2), c2(4, -1);
Complex cc;
cc = c1 + c2;
cout << "c1+c2=";
cc.display();
return 0;
}
运行结果:
对下面代码进行比较:
class Complex{
public:
Complex add(Complex& c2);
Complex operator+(Complex& c2);
private:
...
};
Complex Complex::add(Complex& c2){
Complex c;
c.real = real + c2.real;
c.imag = imag + c2.imag;
return c;
}
Complex complex::operator+(Complex& c2){
Complex c;
c.real = real + c2.real;
c.imag = imag + c2.imag;
return c;
}
int main(){
Complex c1(1,2),c2(3,4),c3,c4;
c3=c1.add(c2);
c3=c1+c2;
}
在计算c3中我们调用了一个add()加法函数,而在计算c4时,C++编译器将其翻译成c1.operator+(c2),进行两个复数相加。
c.重载运算符的规则
- 不能通过连接其他符号来创建新的操作符:比如operator@,只能对已有的运算符进行重载
- 重载不能改变运算符运算对象的个数(重载运算符的函数不能有默认的参数,否则就会改变运运算符参数的个数),不能改变运算符的优先级别,不能改变运算符的结合性
- 重载操作符必须有一个类类型或者枚举类型的操作数
- 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义
- 用于对象的运算符一般必须重载,但有两个例外,运算符"="和"&"一般不用用户进行重载
- 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参
- .*、::、sizeof、?:、. 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
d.赋值运算符的重载
在上面的知识点3中,我们知道一般情况下"="不必要用户进行重载,但是当系统提供给的默认的对象赋值运算符不能满足时,,例如数据成员中包含指向动态分配的指针成员时,在复制此成员是可能存在危险,就需要自己重载赋值运算符。
***了解运算符重载中使用引用的重要性:利用引用作为函数参数的形参可以再调用函数的过程中不适用传递值的方式进行虚实结合,而是通过传址方式使形参成为实参的别名,因此不生成临时变量(实参的副本),因此减少了时间和空间的开销。此外重载函数的返回值是对象的引用时,返回的不是常量,而是引用所代表的对象,它可以成为赋值运算符的左值,可以被赋值或者参与其他操作,但是要注意的是修改可引用,就等于修改可他所代表的对象。***
5.3取地址重载
在这里我们对普通对象取地址就不多说了,直接看对const取地址操作符重载。
5.3.1const对象取地址
a.const成员函数
在之前我哦们就已经知道const关键字,在C中我们常用#define指令爱定义符号常量,但是用这种方法容易出错,因此C++提供了const定义常变量的方法。const常与指针结合。const可以用来修饰成员函数和成员变量。这里用const修饰的成员函数称之为const成员函数,实际上,const修饰的是成员函数的隐含的this指针,表明该成员函数中不能对任何成员进行修改。
先举个栗子:
class A{
public:
int T1();
};
//例1
const int T1(const int a) const{
...}
//例2
const A& T2(const A& a) const{
...}
我们现在看上述例1的代码,出现了3个const,第一const修饰的是返回值,第二个const修饰的是函数的参数,第三个const修饰的是成员函数本身(也就是所谓的const成员函数)。将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this 指针,表明在该成员函数中不能对类的任何成员进行修改。
我们此时再看例2的代码,该函数隐式的访问一个对象,而显式的访问了另一个对象,并返回其中一个对象的引用。第一个const表示返回const引用,第二个const表示该函数不会修改被显式地访问的对象,第三个const表明该函数不会修改被隐式的访问的对象。
我们现在根据几个问题来思考:
const对象可以调用非const成员函数吗?
直接上代码:
因此我们可以知道,const对象不可以调用非const成员函数,但是非const对象可以调用const成员函数,因为非const对象的权限更大。
const成员函数内可以调用其他非const成员函数吗?
由此可见,编译不能通过,但是在非const成员函数中调用const成员函数是可以的。
在const成员函数中如果要进行修改的话,就必须使用关键字mutable进行修饰。
b.const成员变量
const修饰成员变量相当于该变量是一个常量,所以只能在初始化列表上初始化。
举个栗子:
class Date{
public:
Date(int y = 1900,int m = 1,int d = 1):year(y),month(m)day(d){}
private:
int year;
int month;
int day;
};
六.static成员
如果想在同类的多个对象之间实现数据共享,不想用全局对象,可以使用静态数据成员。
6.1静态数据成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量。
6.1.1静态数据成员形式
数据类型 类名::静态数据成员 = 初值;
int Date::year = 1900;
6.1.2静态数据成员的特点
- 静态数据成员只占一份空间,每个对象都可以引用这和静态数据成员。静态数据成员的值对所有对象都是一样的,改变它的值所有对象中的这个数据成员的值都会改变。静态数据成员不属于某一个对象,在为对象分配的空间中不包括静态数据成员所占的空间。静态数据成员实在所有对象之外单独开辟空间。
- 静态数据成员是在程序编译时分配的空间,在程序结束时才释放空间。
- 静态数据成员可以进行初始化,但只能在类外进行初始化。
- 在类外可以通过对象名引用公用的静态数据成员,也可以通过类名引用静态数据成员。(即使没有定义对象,也可以通过类名来引用静态数据成员,这说明静态数据成员并不属于对象,而是属于类的。)
- 有了静态数据成员,不必再使用全局变量,全局变量破坏了封装性原则。但也要注意,静态数据成员的作用域只限定于定义该类的作用域内。
6.2静态成员函数
用static修饰的成员函数,称之为静态成员函数。
class Date{
public:
static void get_year();
private:
int year;
};
Date::get_year(){
cout<<year<<endl;
}
静态成员函数是类的一部分而不是对象的一部分,如果要在类外调用公有的静态成员函数,要用类名和域作用符"::"。与静态数据成员不同,静态成员函数的作用是为了能处理静态数据成员。静态成员函数与非静态成员函数最大的区别就是,静态成员函数没有this指针,而非静态成员函数有this指针,由此决定了静态函数无法访问本类中的非静态成员。
七.友元
友元分为:友元函数和友元类 友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
7.1友元函数
如果在本类以外的其他地方定义了一个函数,再累体内用friend进行声明,就将其称之为友元函数。友元函数可以访问这个类内的私有成员。
例1:
//将普通函数声明成友元函数
#include<iostream>
using namespace std;
class Date{
public:
firend void display(Date&);//声明display()是Date类的友元函数
private:
int year;
};
void display(Date& y){
cout<<y.year<<endl;
}
例二:
calss A;
class B{
pbulic:
void display(A&);//display()是成员函数,形参是Date类对象的引用
private:
int b;
};
class A{
public:
friend void B::display(A&);//声明B中的display()是本类中的友元成员函数
private:
int a;
};
注意:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用和原理相同
7.2友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。 友元关系是单向的,不具有交换性。 比如上述B类和A类,在B类中声明A类为其友元类,那么可以在A类中直接访问C 类的私有成员变量,但想在C类中访问B类中私有的成员变量则不行。 友元关系不能传递 如果B是A的友元,C是A的友元,则不能说明C时B的友元。
声明友元类的一般形式:
friend B;
//friend 类名;
***友元关系是单向而不是双向,友元的关系不能传递。***
以上是关于C++类和对象的主要内容,如果未能解决你的问题,请参考以下文章