梅林做网站,wordpress 宅男猫网站,网站建设百度,上海微网站制作一.类中生成的默认成员函数详解
0.类的6个默认成员函数
编译器会给类生成六个默认成员函数#xff0c;在类中即使我们什么都不做#xff0c;也会自动生成。
默认成员函数#xff1a;用户没有显式实现#xff0c;编译器会自动生成的成员函数称为默认成员函数。 下面我们逐…一.类中生成的默认成员函数详解
0.类的6个默认成员函数
编译器会给类生成六个默认成员函数在类中即使我们什么都不做也会自动生成。
默认成员函数用户没有显式实现编译器会自动生成的成员函数称为默认成员函数。 下面我们逐个来讲解这些函数。
1.构造函数
1.1概念
对于以下Date类
class Date
{
public:void Init(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;
};
对于我们写的这个类来说我们使用时需要用Init的公有方法给对象设置日期但是如果我们每次都要使用Init方法给对象设置日期信息未免有些过于繁琐那么有没有办法能够在对象创建时就将信息设置进去呢
构造函数即可完成这件事情。
构造函数是一个特殊的成员函数它的名字和类名相同在创建类类型对象时编译器会自动调用以保证每个数据成员都有一个合适的初始值并且在对象整个生命周期内只调用一次。
1.2特性
构造函数虽然叫构造函数但其作用并不是开一个空间来创建对象而是初始化对象。
其特征如下
函数名与类名相同无返回值对象实例化时编译器会自动调用相应的构造函数构造函数可以重载
下面我们来验证一下以上四个特性
class Date
{
public:Date()//无返回值而且函数名与类名相同{_year 2024;_month 5;_day 28;}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;
};
int main()
{Date A;//调用无参构造A.Print();Date B(3, 4, 5);//调用三个参数的构造B.Print();return 0;
} 在上面这段代码中通过打印我们可以观察到一个调用了无参构造一个调用了有参构造。 如果类中没有显式定义构造函数则C编译器会自动生成一个无参的默认构造函数一旦用户显式定义编译器将不再生成默认构造函数。 可以看到我们在将构造函数屏蔽了之后依旧是可以通过编译的因此当我们不显式定义构造函数时编译器会自动生成默认构造函数。
现在我们执行下这段代码 我们发现了一个奇怪的现象这里生成了随机值那么默认构造函数做了什么事呢 答案C将类型分为了内置类型和自定义类型内置类型就是语言提供的类型而自定义类型则是我们使用class/union/struct等关键字自己定义的类型。 而默认构造函数会对自定义类型成员调用它的默认构造函数。 我们可以用以下代码验证一下
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 _a;
}; 我们在main函数中初始化了一个Date类型的对象而Date类型的对象中声明了一个Time类型的对象由于我们没有写Date类的默认构造函数因此它会调用编译器自动生成的默认构造函数这时默认的构造函数所作的事情就是去调用Date类中的自定义类型的构造函数。而Time类的构函数中打印了Time,因此这段程序的最终输出结果为上图所示。
注意点在C11版本中为我们无法对内置类型生成默认构造的问题打了一个补丁我们可以在声明时给一个缺省值。
如下图所示 无参的构造函数和全缺省的构造函数都被称为默认构造函数并且默认构造函数只能有一个。
主要注意的是无参的构造函数、全缺省的构造函数、编译器生成的构造函数只能有一个否则会有调用歧义。
譬如下图代码编译器则会不知道调用的是哪个函数。 总结不用传递参数就可以调用的构造函数就是默认构造函数。
2.析构函数
2.1概念
刚刚我们学习了构造函数现在我们已经知道一个对象是怎么来的了那么它是怎么没的呢这就需要析构函数来大显神威了
析构函数与构造函数功能相反析构函数不是完成对对象的销毁局部对象的销毁工作生命周期是由编译器完成的。而对象在销毁时会自动调用析构函数完成对象中资源的清理工作。
2.2特性
析构函数是特殊的成员函数其特征如下:
析构函数名是在类名前加上~无参数无返回值类型一个类只能有一个析构函数。若未显式定义系统会自动生成默认的析构函数。析构函数不能重载对象生命周期结束时C编译系统自动调用析构函数。
下面我们还是来使用一下它
typedef int DataType;
class STack
{
public:STack(size_t capacity 3){_a (DataType*)malloc(sizeof(DataType) * capacity);if (!_a){perror(malloc fail!);return;}_capacity capacity;_size 0;}void Push(DataType data){_a[_size] data;_size;}~STack(){if (_a){cout 我析构啦! endl;free(_a);_a NULL;_capacity 0;_size 0;}}
private:DataType* _a;int _capacity;int _size;
};
int main()
{STack s;s.Push(1);
} 这里我们可以发现当s的生命周期要结束时编译器自动调用了析构函数。
与构造函数类似编译器生成的析构函数也只会调用自定义类型的析构函数
class Time
{
public:
~Time()
{
cout 我析构啦 endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year 1970;
int _month 1;
int _day 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
} 这里不再画序号帮助大家理解过程而是通过文字帮助大家深入理解原理 在main方法中根本没有直接创建Time类的对象为什么最后会调用Time类的析构函数 原因main方法中创建了Date对象d而d中包含4个成员变量其中_year, _month, _day三个是内置类型成员销毁时不需要资源清理最后系统直接将其内存回收即可而_t是Time类对象所以在d销毁时要将其内部包含的Time类的_t对象销毁所以要调用Time类的析构函数。但是main函数中不能直接调用Time类的析构函数实际要释放的是Date类对象所以编译器会调用Date类的析构函数而Date没有显式提供则编译器会给Date类生成一个默认的析构函数目的是在其内部调用Time类的析构函数即当Date对象销毁时要保证其内部每个自定义对象都可以正确销毁。main函数中并没有直接调用Time类析构函数而是显式调用编译器为Date类生成的默认析构函数。 注意创建哪个类的对象则调用该类的析构函数销毁那个类的对象则调用该类的析构函数 如果类中没有在堆上申请资源析构函数可以不写直接使用编译器生成的默认析构函数比如Date类有资源申请时一定要写否则会造成资源泄漏比如Stack类。。
3.拷贝构造函数
3.1概念
在现实生活中我们在超市买小面包可以认为同一种类型的两袋小面包是一样的。
那么有没有一样的对象呢我们能否创建一个跟已有对象一样的对象呢
这就引入了我们的拷贝构造函数。
拷贝构造函数只有单个形参该形参是对本类类型对象的引用一般常用const修饰再用已存在的类类型对象创建新对象时由编译器自动调用。
3.2特性
拷贝构造函数是构造函数的一个重载形式。拷贝构造函数的参数只有一个而且必须是类类型对象的引用使用传值方式编译器会报错因为会引发无穷递归调用。
class Date
{
public:
Date(int year 1900, int month 1, int day 1)
{
_year year;
_month month;
_day day;
}
// Date(const Date d) // 错误写法编译报错会引发无穷递归
Date(const Date d) // 正确写法。
{
_year d._year;
_month d._month;
_day d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
} 若未显式定义编译器会生成默认的拷贝构造函数。 默认的拷贝构造对象按照内存存储字节序完成拷贝这种拷贝叫做浅拷贝或者值拷贝。和memcpy的拷贝模式一样。
对于我们的Date类来说值拷贝就已经够用了但是别的类呢就譬如说我们的栈
typedef int DataType;
class Stack
{
public:Stack(size_t capacity 10){_array (DataType*)malloc(capacity * sizeof(DataType));if (nullptr _array){perror(malloc申请空间失败);return;}_size 0;_capacity capacity;}void Push(const DataType data){// CheckCapacity();_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;
} 我们在运行这段代码时会发现它崩溃了这是为什么呢我们的s1对象中使用malloc动态开辟了一块内存而我们是字节序拷贝的因此我们的s2的array数组也指向了这块内存而在程序退出时s2和s1都要销毁此时s2先销毁s1再销毁原因是栈区先进后出s2已经将malloc的那块内存释放掉了s1再去释放就会崩溃了。 因此我们可以得到如此一个结论类中如果没有涉及到资源的申请时拷贝构造函数是否写都可以一旦涉及到资源申请时则拷贝构造函数必须要写。
3.3拷贝构造的使用方法
我们的拷贝构造有两种写法分别如下
Stack s2(s1);//新创一个对象s2并将s1拷贝给s2。
Stack s2 s1;//这样也是可以完成拷贝的。
4.运算符重载
C为了增强代码的可读性引入了运算符重载运算符重载是具有特殊函数名的函数也具有其返回值类型函数名字以及参数列表其返回值类型与参数列表与普通的函数类似。 函数名字为关键字operator后面接需要重载的运算符符号。 函数原型返回值类型 operator操作符(参数列表)
注意点
不能通过连接其他符号来创建新的操作符比如operator重载操作符必须有一个类类型参数用于内置类型的运算符其含义不能改变例如内置的整型不 能改变其含义作为类成员函数重载时其形参看起来比操作数数目少1因为成员函数的第一个参数为隐藏的this。.* :: sizeof ?: . 注意以上5个运算符不能重载。
现在我们来重载一个运算符
//类内--成员函数
bool operator(const Date d)
{return _year d._year _month d._month _day d._day;
}
//类外--全局函数
bool operator(const Date d1, const Date d2)
{
return d1._year d2._yeard1._month d2._monthd1._day d2._day;
}在这里提一句题外话我们在写代码的途中能用引用返回或者引用传值时就尽量用引用因为它它的效率会比较高。至于能否用引用返回这需要我们关注他们的生命周期。
5.赋值运算符重载
参数类型const Date 传递引用可以提高传参效率返回值类型 Date返回引用可以提高返回的效率有返回值的目的是为了方便连续赋值检测是否自己给自己赋值返回*this以满足连续赋值。
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;}//成员函数--第一个参数是隐藏的this指针Date operator(const Date d){if (this ! d){_year d._year;_month d._month;_day d._day;}return *this;}
private:int _year;int _month;int _day;
};
//全局函数--没有隐式的this需要传递两个参数。
Date operator(const Date Mythis,const Date d)
{if (Mythis ! d){Mythis._year d._year;Mythis._month d._month;Mythis._day d._day;}return *Mythis;
}
按理来说上述的代码是可以跑的过去的但是实际上会产生编译错误。 为什么赋值运算符必须是成员函数呢
这是因为赋值运算符如果不显式实现编译器会生成一个默认的赋值运算符此时用户再在类外自己实现一个全局的赋值运算符重载就会和编译器在类内生成的默认赋值运算符重载冲突。因此运算符重载只能是类的成员函数。
如果我们不显式实现赋值运算符的话编译器生成的赋值运算符是以值的方式逐字节拷贝。
注意
1.内置类型成员变量是直接赋值的而自定义类型的成员变量需要调用对应的类的赋值运算符重载完成赋值。需要深拷贝的类需要我们手写赋值运算符浅拷贝即可解决需求的类用默认生成的即可。
关于运算符重载的知识这里不再阐述博主会在后续写一篇日期类的实现讲运算符重载更加深入的讲解。
6.const修饰函数
如果我们想要用const修饰类内某个成员函数的this指针应该如何实现呢
CPP将const修饰的成员函数称为const成员函数而const修饰类成员函数实际上修饰的是该成员函数隐含的this指针表明在该成员函数中不能对类的任何成员进行修改。 譬如这段代码
class Date
{
public:Date(int year, int month, int day){_year year;_month month;_day day;}void Print(){cout Print() endl;cout year: _year endl;cout month: _month endl;cout day: _day endl endl;}
private:int _year; // 年int _month; // 月int _day; // 日
};int main()
{const Date d2(2022, 1, 13);d2.Print();//errorreturn 0;
}由于我们传递的d2是const的但是在调用Print函数时*this并没有加const修饰这就造成了权限的放大是不被允许的。因此我们应当将Print函数加上const修饰。 void Print() const{cout Print() endl;cout year: _year endl;cout month: _month endl;cout day: _day endl endl;}7.取地址及const取地址操作符重载
取地址()也是可以重载的不过一般情况下不需要我们写重载函数。
class Date
{
public:Date* operator(){return this;}const Date* operator()const{return this;}
private:int _year; // 年int _month; // 月int _day; // 日
};二.struct和class的对比
1struct
1.1struct的成员
在C中struct中不仅可以放数据类型而且可以放函数。这个函数可以在类内定义也可以在类外定义。
类内定义
#include iostream
using namespace std;
struct kuzi
{int Add(int a, int b){return a b;}
};
int main()
{struct kuzi trousers;printf(%d, trousers.Add(3, 2));return 0;
} 类外定义 1.1.2结构体的默认访问权限
在结构体中为了兼容C语言数据成员的默认访问权限为public这也就代表着它们可以在结构体的外部直接进行访问。
如以下代码我们可以直接给trousers这个对象的a和b进行赋值。
struct kuzi
{int a;int b;
};
int main()
{struct kuzi trousers;trousers.a 3;trousers.b 5;return 0;
} 1.1.3访问权限控制
在C11的标准中引入了结构体成员的引用控制修饰符publicprivataprotect我们可以显式的控制它的成员的访问权限。
struct kuzi
{//默认是publicint a;int b;
private:int c;int d;
}; 1.2.class
在C中类class是一种重要的概念可以创建用户定义的数据类型其中可以包括数据成员和成员函数。而且C中的类支持继承和多态等面向对象的概念允许你通过基类创建派生类实现代码的扩展和重用。剩下的知识点已经讲解过了这里不再赘述。
1.3结构体Struct和类Class之间的区别以及各自使用场景
C中的结构体struct和类class有一些相似之处但也存在一些关键的区别。以下是结构体和类之间的主要区别
1、默认访问权限
结构体的成员默认访问权限是公共public因此结构体的成员在外部可以直接访问。 类的成员默认访问权限是私有private因此类的成员在外部不能直接访问需要通过公共的成员函数来访问。2、成员函数
类可以包含成员函数这些函数可以操作类的私有成员并且可以实现类的行为和功能。 结构体也可以有成员函数但是它们的主要目的是为了实现一些操作而不是类似于类的行为。3、继承
类可以通过继承实现子类与父类之间的关系可以使用公共、保护或私有继承来控制成员的访问权限。 结构体也可以继承但由于其成员默认是公共的继承可能导致访问权限问题当然我们也可以控制其访问权限让其可以继承。4、构造函数和析构函数
类拥有构造函数和析构函数用于对象的初始化和清理。 结构体也可以有构造函数和析构函数但不会默认生成但是它们的使用场景通常是比较简单的数据封装。5、默认成员访问标签Access Labels
在类和结构体中可以使用访问标签public、private、protected来指定成员的访问权限。6、 使用场景
结构体的使用场景
用于存储一组相关的数据但没有复杂的操作和逻辑。类的使用场景
当你需要封装数据并附加操作和行为时类更适合因为它可以将数据和操作封装在一起。 在实现更复杂的数据结构如树、图等使用类也更加合适。 总结 虽然结构体和类在某些方面很相似但它们的默认行为、访问权限、使用场景以及是否支持面向对象编程的特性如继承、多态等都有一些差异。在选择使用结构体还是类时需要考虑你的代码的需求和设计目标。但是结构体可以做的事类都可以干结构体不可以干的事类也都可以干 码字不易如果你觉得博主写的不错给个三联关注评论把