您现在的位置是:首页 >技术杂谈 >【C++】类和对象(二)网站首页技术杂谈
【C++】类和对象(二)
目录
?一.类的6个默认成员函数?
在上篇文章中我们谈到了空类,即类里面什么都没有,我们把类中没有任何成员的类称之为空类
空类真的空吗?空类真的里面什么都没有?
答案是否定的,即使类里面什么都不写,编译器也会默认生成6个成员函数,由编译器自动生成的成员函数称之为默认成员函数
6个默认成员函数:初始化、清理,拷贝、赋值,取地址,重载
示例图如下
?二.构造函数?
2.1构造函数的概念
我们在之前学习数据结构时,实现数据结构的时候都会有一个初始化函数,往往在定义后需要手动调用初始化函数,那么我们可以定义的时候同时初始化呢
C++中引入了构造函数,构造函数是一个特殊的成员函数,名字与类名相同,创建对象时自动调用,以对成员变量进行初始化,并且定义之后只会调用一次
2.2构造函数的特性
构造函数名字虽然名字是构造,其功能并不是创建一个对象,而是初始化对象
构造函数的函数名与类名相同,并且无返回值,并不是写void,而是没有返回值这个选项
构造函数可以重载,即可以按需实现不同的初始化
构造函数是由编译器自动调用的
构造函数的重载
#include<iostream>
using namespace std;
class Date
{
public:
Date()
{
cout << "无参构造" << endl;
}
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
cout << "带参构造" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//调用无参构造
Date d2(2023, 5, 19);//调用有参构造
return 0;
}
运行结果:
可以看到当我们定义一个对象不传参数时,编译器会调用无参构造,当定义对象时传参就会调用带参构造
但是既然涉及到函数重载,那么不得不提到的二义性就是带有默认参数的函数和无参函数的冲突的问题
在上面的基础上,我们加一个全默认参数的构造函数
#include<iostream>
using namespace std;
class Date
{
public:
//无参构造
Date()
{
cout << "无参构造" << endl;
}
//带参构造
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
cout << "带参构造" << endl;
}
//全默认参数构造
Date(int year = 2023, int month = 5, int day = 19)
{
_year = year;
_month = month;
_day = day;
cout << "全默认参数构造" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//到底是调用无参构造还是全默认参数构造呢?
Date d2(2023, 5, 19);//调用有参构造
return 0;
}
编译报错信息:
报错信息显示对重载函数的调用不明确,即调用构造的时候出现二义性,因此我们以后自己定义构造函数应该避免这种二义性,即无参构造函数和全默认参数构造函数不能同时出现
默认构造函数
当我们没有定义构造函数时,编译器会自动生成一个无参的默认的构造函数,如果我们已经定义了构造函数,则编译器将不再生成
编译器自动生成的、无参的构造函数、全默认参数的构造函数,这三者都称之为默认构造函数,显然,这三者在一个类中只能存在一种,一般建议类中包含一个默认构造函数
编译器生成的默认构造函数有什么用呢,我们来看下面的代码
#include<iostream>
using namespace std;
class Time
{
public:
Time()
{
cout << "Time()" << endl;
}
private:
int _hour;
int _minute;
int _sce;
};
class Date
{
public:
//自己没有定义构造函数,因此编译器会自动生成一个
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
//内置类型
int _year;
int _month;
int _day;
//自定义类型
Time ti;
};
int main()
{
Date d1;
d1.Print();
return 0;
我们声明一个时间Time类并定义了它的构造函数,然后声明了一个日期Date类,其成员变量有它自己的日期即内置类型,也有时间类即自定义类型,我们来看编译器默认生成的构造函数会做什么处理
运行结果
可以看到,对于内置类型,编译器自动生成的构造函数并没有处理,而对于自定义类型,会去调用它的默认构造函数
注意:调用默认构造不需要在定义对象时在对象后面加一个空括号(),因为这样编译器会把它当成函数的声明,而不是对象的定义
?三.析构函数?
3.1析构函数的概念
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作
3.2析构函数的特性
析构函数是特殊的成员函数,其有如下的特点:
1.析构函数的函数名为类名前加~字符,~字符我们知道是按位取反的功能,此处就是顾名思义,与构造函数功能相反
2.和构造函数一样,没有返回值,但析构函数没有参数
3.析构函数不能重载,即一个类只能有一个析构函数,如果自己没有定义析构函数,则编译器自动生成,如果已经定义则编译器不再生成
4.对象声明周期结束时,会自动调用析构函数
5.编译器自动生成的析构函数的作用,我们来看以下代码
#include<iostream>
using namespace std;
class Time
{
public:
Time()
{
cout << "Time的构造函数" << endl;
}
~Time()
{
cout << "Time的析构函数" << endl;
}
private:
int _hour;
int _minute;
int _sce;
};
class Date
{
public:
//自己没有定义构造函数,因此编译器会自动生成一个
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
//内置类型
int _year;
int _month;
int _day;
//自定义类型
Time ti;
};
int main()
{
Date d1;
return 0;
}
我们声明一个时间Time类并定义了它的构造函数,然后声明了一个日期Date类,其成员变量有它自己的日期即内置类型,也有时间类即自定义类型,我们来看编译器默认生成的析构函数会做什么处理
运行结果
可以看到类似于构造函数,析构函数对内置类型不做处理,对自定义类型会调用它的析构函数
6.如果类中没有申请资源时,可以不写析构函数,即使用编译器自动生成的即可,对于有自愿申请如在堆上开辟空间等,则需要自己学析构函数,不然会造成内存泄漏
7.析构函数调佣的顺序为:先构造的后析构,后构造的先析构
代码:
#include<iostream>
using namespace std;
class Time
{
public:
Time()
{
cout << "Time的构造函数" << endl;
}
~Time()
{
cout << "Time的析构函数" << endl;
}
private:
int _hour;
int _minute;
int _sce;
};
class Date
{
public:
Date()
{
cout << "Date的构造函数" << endl;
}
~Date()
{
cout << "Date的析构函数" << endl;
}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Time t1;
Date d1;
return 0;
}
先定义了一个时间对象,后定义了一个日期对象,运行结果
可以看到,构造顺序为先构造Time,后构造Date,而析构的顺序为先析构Date,后析构Time,即先构造的后析构,后构造的先析构。 这是由于局部对象存储在栈区,栈区有类似于数据结构的栈的特性,即先进后出后进先出
?四.拷贝构造函数?
4.1拷贝构造函数的概念
在日常生活中,我们有时候会见到双胞胎,即两个人长得几乎一模一样,那在创建对象时,能否用一个已经存在的对象去创建一个新对象,使得两个对象一模一样呢?
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
4.2拷贝构造函数的特性
拷贝函数是特殊的成员函数,其有如下的特点:
1.拷贝构造函数是构造函数的一个重载形式
2.拷贝构造函数的参数只有一个且必须是对象的引用,如果使用传值方式会引发无情递归而报错
这是因为传参时形参是实参的拷贝,由于是对象的拷贝,因此传值过程中会调用拷贝构造函数,然后再次进行传值,而这次传值又是一次拷贝即调用拷贝构造函数,因此会无穷地递归下去
3.类似于构造函数和析构函数,如果用户没有定义则编译器会生成默认的拷贝构造函数。默认拷贝构造函数是按照字节进行拷贝的,类似于C语言中memcpy函数,这种拷贝称为浅拷贝
默认拷贝构造函数对于内置类型按照字节进浅拷贝,对于自定义类型会调用它的拷贝构造函数
代码:
#include<iostream>
using namespace std;
class Time
{
public:
Time()
{
cout << "Time的构造函数" << endl;
}
Time(const Time& t)
{
cout << "Time的拷贝构造函数" << endl;
}
private:
int _hour;
int _minute;
int _sce;
};
class Date
{
public:
Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
Time t;
};
int main()
{
Date d1(2020,5,23);
d1.Print();
Date d2(d1);
d2.Print();
return 0;
}
我们先定义了日期对象d1,然后用d1拷贝构造d2,运行结果
可以看到对于内置类型,默认拷贝构造按照字节拷贝成功,对于自定义类型,编译器去调用其拷贝构造函数
对于内置类型,按照字节拷贝已经能够顺利拷贝,那么我们还需要自己定义拷贝构造函数吗
分析一下代码:
#include<iostream>
using namespace std;
typedef int DataType;
class Stack
{
public:
Stack(int capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}
运行结果
程序崩溃了,以上代码由于对象在堆上申请了空间,在拷贝时按照字节拷贝,即两个对象的array指针的值是一样的,即两个指针指向用一块空间
带来的问题:
插入数据会相互影响
对同一块空间释放两次,造成程序崩溃
因此,如果类中没有申请资源时,拷贝构造函数可写可不写,对于资源申请的类,则必须需要自己写拷贝构造函数,否则默认拷贝构造函数即浅拷贝会带来问题
4.拷贝构造函数调用的场景
使用已存在对象创建新对象
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& t)
{
_year = t._year;
_month = t._month;
_day = t._day;
cout << "Date的拷贝构造函数" << endl;
}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2020,5,23);
d1.Print();
Date d2(d1);
d2.Print();
return 0;
}
运行结果
函数参数为对象
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& t)
{
_year = t._year;
_month = t._month;
_day = t._day;
cout << "Date的拷贝构造函数" << endl;
}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void fun(Date t)
{
}
int main()
{
Date d1(2023,5,23);
fun(d1);
return 0;
}
运行结果
函数返回值为对象
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& t)
{
_year = t._year;
_month = t._month;
_day = t._day;
cout << "Date的拷贝构造函数" << endl;
}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
Date fun()
{
Date d1(2023,5,23);
return d1;
}
int main()
{
fun();
return 0;
}
运行结果
为了提高程序的效率,减少拷贝过程,对象传参尽量使用引用,函数返回值能够用引用返回就用引用返回
?五.赋值运算符重载?
5.1运算符重载
C++为了增强代码的可读性,引入了运算符重载,运算符重载是具有特殊函数名的函数
语法:返回值类型 operator运算符(参数列表)
对于内置类型比如整形等,加减乘除等运算符可以直接对它们进行计算,但是可以实现两个对象用+运算符进行相加吗,显然是不能的,这时候就需要利用运算符重载
运算符重载的特点:
1.重载运算符必须有一个对象类型参数
2.用于内置类型的运算符不能改变其含义
3.运算符重载可以重载为全局函数和对象的成员函数
重载为全局函数 ,以重载运算符-为例
#include<iostream>
using namespace std;
class Student
{
public:
Student()
{
}
Student(int score)
{
_score = score;
}
Student(const Student & t)
{
_score = t._score;
}
void Print()
{
cout << "成绩" <<_score<<endl;
}
private:
int _score;
};
int operator-(const Student& s1, const Student& s2)
{
return s1._score - s2._score;
}
int main()
{
Student st1(100);
Student st2(60);
cout << st1 - st2 << endl;
return 0;
}
可以看到重载为全局函数会出现无法访问的情况,因为成员变量是私有的,在类外不能直接访问
解决办法:
将运算符重载函数声明为类的友元函数
将成员变量设置为共有
通过类中的成员函数访问私有的成员变量
前面两种方法破坏了类的封装性,故推荐第三种,即通过成员函数访问私有的成员变量
重载为类的成员函数,同样以运算符-为例
#include<iostream>
using namespace std;
class Student
{
public:
int operator-( const Student& s2)
{
return _score - s2._score;
}
Student()
{
}
Student(int score)
{
_score = score;
}
Student(const Student & t)
{
_score = t._score;
}
void Print()
{
cout << "成绩" <<_score<<endl;
}
private:
int _score;
};
int main()
{
Student st1(100);
Student st2(60);
cout << st1 - st2 << endl;//调用opeartor-函数
return 0;
}
通过与全局函数对比可以发现,重载为成员函数少了一个参数,因为类的非静态成员函数有隐藏的this指针
4 .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常出现在笔试题中
5.2赋值运算符重载
1.赋值运算符重载格式:
返回值类型:T &,返回引用提高效率
函数参数:const T &,传递引用可以提高效率
返回*this,以满足赋值的连续性
检测是否自己为自己赋值
include<iostream>
using namespace std;
class Student
{
public:
int operator-( const Student& s2)
{
return _score - s2._score;
}
Student& operator=(const Student& s)
{
if (this != &s)
{
_score = s._score;
}
return *this;
}
Student()
{
}
Student(int score)
{
_score = score;
}
Student(const Student & t)
{
_score = t._score;
}
void Print()
{
cout << "成绩" <<_score<<endl;
}
private:
int _score;
};
int main()
{
Student st1(100);
Student st2(60);
cout << st1 - st2 << endl;
st1 = st2;//调用operator=函数
return 0;
}
原因是如果类中没有赋值运算符重载函数,编译器会默认生成一个,与重载的全局函数发生冲突
3.当用户没有在类中定义运算符重载函数时,编译器会默认生成一个,类似于拷贝函数,以字节拷贝的方式进行拷贝,对于内置类型会进行浅拷贝,对于自定义类型会去调用它的赋值运算符重载函数,这种浅拷贝对于没有进行资源申请的内置类型是可行的,但是对于数据结构的栈这种进行资源申请的类则会出现问题,因此如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
5.3前置++和后置++重载
1.前置++的格式:引用返回 opeartor++();
Student& operator++()
{
_score++;
return *this;
}
2.后置++的格式:引用返回 opeartor++(int),C++规定后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
函数:
Student& operator++(int)
{
Student temp(_score);
_score++;
return temp;
}
调用处:
st1++;
编译器优化:
operator++(0)
前置++和后置++的区别就在于函数参数后者有一个int的占位参数,调用的时候有编译器自动识别并调用,前置--和后置--同理
?6.const成员?
6.1 const成员函数
const成员函数:const修饰的成员函数称之为const成员函数,const放在函数参数列表后面,格式如下:返回值类型 函数名(参数列表)const
const修饰成员函数,实际上是修饰隐藏的this指针,使得this指针变为常量指针,即不可以改变成员变量
6.2const对象
const对象:在定义对象时用const修饰的对象称之为const对象,类似于const修饰的变量,其权限为只读,不能修改,cosnt对象里面的成员变量全都为只读变量
1.const对象调用非const函数
#include<iostream>
using namespace std;
class Student
{
public:
Student()
{
}
Student(int score)
{
_score = score;
}
Student(const Student & t)
{
_score = t._score;
}
void Print()
{
cout << "成绩:" << _score << endl;
}
private:
int _score;
};
int main()
{
const Student st1(100);//定义const对象
st1.Print();//const对象调用非const函数
return 0;
}
const对象的成员变量为只读,而调用的函数的权限为可读可写,属于权限放大,故报错
报错信息
2.const对象调用const函数
#include<iostream>
using namespace std;
class Student
{
public:
Student()
{
}
Student(int score)
{
_score = score;
}
Student(const Student & t)
{
_score = t._score;
}
void Print()const
{
cout << "成绩:" << _score << endl;
}
private:
int _score;
};
int main()
{
const Student st1(100);//定义const对象
st1.Print();//const对象调用const函数
return 0;
}
const对象的成员变量为只读,cosnt函数的权限也为只读,属于权限平移,故编译通过
运行结果
3.const函数调用其他的非const函数
#include<iostream>
using namespace std;
class Student
{
public:
Student()
{
}
Student(int score)
{
_score = score;
}
Student(const Student & t)
{
_score = t._score;
}
int GetScore()//非const函数
{
return _score;
}
void Print()const
{
cout << GetScore()<< endl;//const函数中调用非const函数
}
private:
int _score;
};
int main()
{
const Student st1(100);//定义const对象
st1.Print();//const函数里面调用非const函数
return 0;
}
const函数内的权限为只读,而在函数中调用非const函数,非const函数的权限为可读可写,属于权限放大,故报错
报错信息
’4.非const函数调用const函数
#include<iostream>
using namespace std;
class Student
{
public:
Student()
{
}
Student(int score)
{
_score = score;
}
Student(const Student & t)
{
_score = t._score;
}
int GetScore()const//const函数
{
return _score;
}
void Print()
{
cout << GetScore()<< endl;//非const函数中调用const函数
}
private:
int _score;
};
int main()
{
Student st1(100);
st1.Print();//const函数里面调用非const函数
return 0;
}
非const函数的权限为可读可写,调用const函数,const函数的权限为只读属于权限缩小,故编译通过
运行结果
总结:const修饰的对象和函数,以及它们与其他非const修饰的函数的互相调用关系,只需判断它们各自的权限,然后判断是否是权限放大,是权限放大则会报错,反之如果是权限平移或者缩小则编译通过
好啦,关于类和对象(二)就先学到这,如果对您有所帮助,欢迎一键三连,您的支持是我创作的最大动力