当前位置: 首页 > news >正文

C++类和对象(中)- 默认成员函数

目录

1. 类的默认成员函数

2. 构造函数

3. 析构函数

4. 拷贝构造函数

5. 运算符重载

5.1 运算符重载

5.2 赋值运算符重载

6. 取地址运算符重载


1. 类的默认成员函数

什么是默认成员函数?即用户没有显示去实现,编译器自动生成的成员函数。但实际上大多数情况会显示写?那这个默认成员函数到底有啥用?别着急,先慢慢看。

一个类,我们不写的情况下编译器会默认生成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要(了解即可)。

这时存在两个疑惑?

1. 编译器自动生成的行为是什么?仅凭默认成员能否完成需求?

2. 什么情况下不会默认生成?不默认生成又该如何处理?

2. 构造函数

构造函数光听名字会觉得是开辟空间构造一个新的对象,但实际上并不是,这里的构造函数主要是初始化(对象实例化完成初始化),相当于我们以前Stack和Date类中写的Init函数的功能。

构造函数的特点:

1. 函数名和类名相同;

2. 不需要返回值(void也不用写,C++规定);

3. 函数实例化时自动调用构造函数;

4. 构造函数可以重载;

5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成;

6. 无参构造函数,全缺省构造函数,不显示写编译器自动生成的构造函数都是默认构造。但是需要注意的是上面的三个默认构造不能同时出现,只能出现一个。比如无参构造函数和全缺省构造函数,在上一节说明了,同时存在编译器不知道调用谁。不显示和前两个不能同时存在应该能理解。总结一下就是不传实参就可以调用的构造就叫默认构造。

class Date
{
public:// 无参函数Date(){_year = 2025;_month = 8;_day = 6;}// 带参函数Date(int year, int month, int day){_year = year;_month = month;_day = day;}// 全缺省函数Date(int year = 2025, int month = 9, int day = 1){_year = year;_month = month;_day = day;}void Print(){cout << _year << " " << _month << " " << _day << " " << endl;}
private:int _year;int _month;int _day;
};int main()
{// 这里不能这样写,无法区分是调用函数还是对象实例化//Date d1();Date d1;d1.Print();return 0;
}

7. 如果我们不写构造函数,那么编译器则会自己生成默认构造,但是编译器生成的默认构造对内置类型不做处理(即内置类型里面的值可能是随机值,具体取决于编译器),对于自定义类型去调用它的默认构造(即要求调用这个成员变量的默认构造函数初始化)。那这个该怎么初始化呢?这个下次讲解(留个悬念)

(注:内置类型(语言提供的原生数据类型):int,char,double....   自定义类型:用class/struct等关键字定义的类型)

3. 析构函数

析构函数看着挺陌生,实际上就是destroy,对资源进行清理和销毁。C++规定对象在销毁时,会自动调用析构函数。就比如之前Stack实现的Destroy功能,而像Date没有Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。

析构函数的特点:

1. 函数名为类名前加字符~ ;

2. 不需要返回值(也不需要写void,C++规定)

3. 一个类只能有一个析构函数,不能析构重载。如果不显示写,系统会自动生成默认的析构函数。

4. 对象生命周期结束,会自动调用析构函数。

5. 如果不写析构函数,编译器会自动生成默认析构。和构造一样,对内置类型不做处理,对于自定义类型会调用它对应的析构函数。

6. 如果显示写了析构,编译器对于自定义类型也会用它的析构函数。总的来说,自定义类型无论是否显示写,都会调用它对应的析构函数。

typedef int STDataType;
class Stack
{
public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}~Stack(){free(_a);_a = nullptr;_capacity = _top = 0;}
private:STDataType* _a;size_t _capacity;size_t _top;
};class MyQueue
{
public:~MyQueue() // 显示写会调用析构,同时自动调用Stack的析构{}
private:Stack pushst;Stack popst;
};
int main()
{//这里不会调用MyQueue的构造和析构,调用Stack的构造和析构MyQueue q1;return 0;
}

7. 如果没有申请资源(空间),编译器自动生成的析构函数就够了。但如果申请了资源,就必须得显示写析构函数,如果利用编译器自动生成的会造成内存泄漏,不安全。

8. 局部定义对象,调用析构的顺序是:先定义的后析构,后定义的先析构。(可以下去调试观看)

4. 拷贝构造函数

什么是拷贝构造?就是用一个已经存在的对象去构造另一个新对象。就比如我去捏瓷器,按照模板照猫画虎搞一个一摸一样的。

拷贝构造函数的特点:

1. 函数名和类名相同,是构造函数的一个重载;

2. 拷贝构造函数的第一个参数必须是类类型的引用,否则编译器会报错。拷贝构造函数也可以多参数,但是第一个参数必须是类类型的引用(忽略隐藏的this指针)

3. C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。

4.  如果不显示写拷贝构造,编译器会自动生成默认拷贝构造,这时内置类型是浅拷贝(值拷贝),自定义类型的拷贝则调用它的拷贝构造。

第2点为什么第一个参数必须是类类型对象的引用?(原因如下)

所以必须引用传参,否则会引发无穷递归。

6. 如果申请了资源,比如向stack和queue申请了空间资源,就得实现深拷贝,否则会出现报错(一块多次析构)。如果一个类显示实现了析构并释放资源,那么他就需要显示写拷贝构造,否则就不需要。

typedef int STDataType;
class Stack
{
public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申请空间失败");return;}_capacity = n;_top = 0;}Stack(const Stack& st){// 开辟一块同样大小的空间_a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);if (_a == nullptr){perror("malloc fail!");}memcpy(_a, st._a, sizeof(STDataType) * st._top);_top = st._top;_capacity = st._capacity;}~Stack(){free(_a);_a = nullptr;_capacity = _top = 0;}
private:STDataType* _a;size_t _capacity;size_t _top;
};Queue{public:private:Stack pushst;Stack popst;};

如果是浅拷贝,则会:

7. 传值返回如果传的是局部对象,则要注意会调用拷贝构造;如果是传引用返回,就不会调用拷贝构造(传的是本身),但如果传的是局部对象的引用返回,则要注意局部对象出作用域会销毁,此时传引用返回就是野引用。传引用返回可以减少拷贝,但是一定要确保返回对象,在当前函数结束后还在,才能用引用返回。

// tmp出add会被销毁,此时是野引用
Date& Func2(){Date tmp(2024, 7, 5);tmp.Print();return tmp;}
int main(){Date d1;d1.Print();Date d2(d1);d2.Print();// 这样写也是拷贝构造Date d3 = d1;d3.Print();Date ret = Func2();ret.Print();return 0;}

5. 运算符重载

5.1 运算符重载

在C语言中比较大小,可以直接通过大于(>),小于(<)等直接进行比较。但是在类类型中是无法直接进行比较的,这时候C++提出了一种新的方法:通过运算符重载的新式赋予新的意义,可以实现类类型的比较。C++规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错。

运算符重载的特点:

1. 函数名由operator和要重载的符号组成,具有其返回类型和参数列表以及函数体;

2. 重载运算符函数的参数个数和该运算符作用的运算对象数量一样多,即一元运算符一个参数,二元运算符有两个参数;

3. 如果运算符重载函数定义在类里面,则要注意:不要忽略隐藏的this指针;

4. 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。

5. 不能通过连接语法中没有的符号来创建新的操作符:比如operator@。

class Date
{
public:Date(int year = 2025, int month = 9, int day = 1){_year = year;_month = month;_day = day;}Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}bool operator==(const Date& d){return _year== d._year&& _month == d._month&& _day == d._day;}void Print(){cout << _year << " " << _month << " " << _day << " " << endl;}~Date(){}
private:int _year;int _month;int _day;
};int main()
{Date d1(2025, 9, 2);Date d2(2024, 6, 1);// << 的优先级比 == 高cout << (d1 == d2) << endl;return 0;
}

6.  .*  ::  sizeof  ?:  .    注意以上5个运算符不能重载。(这里说明一下 .*)

7. 重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义,如: operator+(int x, int y)

8. 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。 C++规定,后置++重载时,增加一个int形参,跟前置++构成函数重载,方便区分。这个int可以传任何值,没有限制。

// ++d1 -> d1.operator++()
date& date::operator++()
{*this += 1;return *this;
}
// d1++ -> d1.operator++(int)
date date::operator++(int)
{date tmp(*this);*this += 1;return tmp;
}

9. 重载<< 和 >>时,需要重载为全局函数。如果在类里面重载,第一个参数默认是this指针,此时cout<<d1就得写为d1<<cout,不符合使用习惯和可读性。重载为全局函数把ostream/istream放到第一个形参位置就可以了,第二个形参位置当类类型对象。)

ostream& operator<<(ostream& out, date& d)
{out << d._year << "年" << d._month << "月" << d._day << "日" << endl;return out;
}istream& operator>>(istream& in, date& d)
{cout << "请依次输入年月日 : > ";in >> d._year >> d._month >> d._day;//输入之后我们需要检查是否合法if (!d.checkdate()){cout << "输入不合法,请重新输入" << endl;}return in;
}
// 如果定义类里面,传参就会变成d1 << cout;

5.2 赋值运算符重载

赋值运算符重载也是默认成员之一,两个已存在的对象的复制拷贝。注意和拷贝构造区别,拷贝构造是用一个对象去拷贝构造另一个对象(构造前还不存在)。

赋值运算符重载的特点:

1. 赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成 const当前类类型引用,否则会传值传参会有拷贝;

2. 如果有返回值,那么最好写引用返回,减少拷贝,提高效率;

3. 如果不显示写拷贝构造,编译器会自动生成,这时内置类型成员变量是浅拷贝(值拷贝),自定义类型成员变量则会调用它对应的运算符重载函数。

4. 和拷贝构造一样,申请了资源得手动实现深拷贝。

	bool operator==(const Date& d){return _year== d._year&& _month == d._month&& _day == d._day;}

注意:

int main()
{Date d1(2025, 9, 2);Date d2(2024, 6, 1);// 赋值重载d1 == d2;// 拷贝构造Date d3 = d1;return 0;
}

6. 取地址运算符重载

取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,一般这两个函数编译器自动生成的就可以够我们用了,不需要去显示实现。

此函数如果要显示写,一般是为了不想让别人拿到地址。

class Date
{
public:Date* operator&(){// return this;return nullptr;}const Date* operator&()const{return this;// return nullptr;}
private:int _year; int _month; int _day; 
};
http://www.xdnf.cn/news/1436257.html

相关文章:

  • 什么是数据库管理系统(DBMS)?RDBMS和NoSQL又是什么?
  • 第 2 讲:Kafka Topic 与 Partition 基础
  • Qwen3-Embedding-0.6B 模型结构
  • Go结构体详解:核心概念与实战技巧
  • Redis-底层数据结构篇
  • MySQL-表的约束(上)
  • 开发中使用——鸿蒙本地存储之收藏功能
  • LLM 能不能发展为 AGI?
  • 开源模型应用落地-模型上下文协议(MCP)-构建AI智能体的“万能插座”-“mcp-use”高级用法(十三)
  • 3.2-C++基础组件
  • 重新审视信任基石:公网IP证书对网络安全生态的影响
  • 【Go语言入门教程】 Go语言的起源与技术特点:从诞生到现代编程利器(一)
  • Cursor 教我学 Python
  • 英伟达Jetson Orin NX-YOLOv8s目标检测模型耗时分析
  • 深度集成Dify API:企业级RAG知识库管理平台解决方案
  • ts,js文件中使用 h函数渲染组件
  • 美国服务器连接速度变慢时应该着重做哪些检查?
  • 双Token实战:从无感刷新到安全防护,完整流程+代码解析
  • PostgreSQL(1) FETCH用法
  • 【MySQL体系结构详解:一条SQL查询的旅程】
  • 《一篇拿下!C++:类和对象(中)构造函数与析构函数》
  • Java 21 虚拟线程 + 分布式调度深度实战:从原理到落地,大促日志同步效率提升 367%
  • 基于SpringBoot的校园资料分享平台
  • Mysql数据库基础(上)
  • 第1章:VisualVM 简介与安装
  • 东土科技战略升级:成立半导体子公司,赋能国产半导体智能化升级
  • 基于 HTML、CSS 和 JavaScript 的智能图像锐化系统
  • HTML第五课:求职登记表
  • 【实时Linux实战系列】基于实时Linux的农业自动化系统开发
  • C++ numeric库简介与使用指南