C++--深入类和对象(下)

news2024/11/24 16:49:01

续接上篇,接着来谈我们的类和对象的深入的知识,话不多说,我们即刻出发......

目录

1.友元

1.1友元函数

输出流运算符的重载

1.2友元类

2.再谈构造函数

2.1构造函数体赋值和初始化列表

构造函数体赋值为何不能叫做初始化?

为何引入初始化列表(对比构造函数体赋值的特性升级)

2.2初始化列表和构造函数体赋值混合使用

2.3 explicit关键字

C++11的隐式类型转换

3.static成员

静态成员函数与非静态成员函数

4.匿名对象

5.拷贝对象时的一些编译器优化


1.友元

      友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。

友元分为:友元函数和友元类

1.1友元函数

输出流运算符的重载

       我们可以尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的 输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作 数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成全局函数,这样才能实现自定义参数顺序的许可。但是这又会导致类外无法访问类成员,所以,我们就需要使用友元,operator>>同理。

cout对象的传值传参和引用传参

       在一般情况下,将 ostream 类型的对象(如 cout)作为传值传参的形式会导致类似的错误,不仅仅限于 operator<< 函数中。

       这是因为 ostream 类型的对象是不可拷贝的,其拷贝构造函数被删除。当使用传值传参方式时,需要进行参数的拷贝构造,而无法调用被删除的拷贝构造函数,因此会导致编译错误

       为了避免这种错误,通常会使用引用传参(如 ostream&)来传递 ostream 类型的对象。引用传参可以直接操作原始对象,而不需要进行拷贝构造。 

operator<<做类内函数无法规则调用

class Date
{
public:
    Date(int year, int month, int day): _year(year), _month(month), _day(day)//初始化列表
    {}

    // d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
    ostream& operator<<(ostream& _cout)//->operator<<(Date *this,ostream& _cout)隐含的this指针被规定在参数最左侧导致调用无法反过来
    {
        _cout << _year << "-" << _month << "-" << _day << endl;
        return _cout;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d(2023, 8, 15);
    d << cout;
    d.operator<<(cout);
    //我们期望的是实现cout<<d,而不是d>>cout,

    return 0;
}

 operator<<做全局函数+类内友元声明

class Date
{
    friend void operator<<(ostream& _cout, const Date& d);
public:
    
    Date(int year, int month, int day): _year(year), _month(month), _day(day)//初始化列表
    {}

    // d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
    //ostream& operator<<(ostream& _cout)//->operator<<(Date *this,ostream& _cout)隐含的this指针被规定在参数最左侧导致调用无法反过来
    //{
    //    _cout << _year << "-" << _month << "-" << _day << endl;
    //    return _cout;
    //}
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d(2023, 8, 15);
    //d << cout;
    //d.operator<<(cout);
    //我们期望的是实现cout<<d,而不是d>>cout,
   
    cout << d;
    return 0;
}
void operator<<(ostream& _cout,const Date &d)//->operator<<(ostream& _cout,Date *this)
{
    _cout << d._year << "-" << d._month << "-" << d._day << endl;
    //return _cout;
}

通过返回ostream对象的引用实现链式操作

     上述的代码,对于我们的重载函数,没有返回值,就导致该操作符输出一次就失效了,也就是说,我们想要一次输出两个对象,就要调用两次函数来实现,比较麻烦,有没有什么更好的办法呢?

       为了实现这样的链式操作,我们将重载函数设置返回值为ostream类型的引用,保证在不调用拷贝构造的情况下,输出完对象,即可返回ostream对象接着等待输出即可。 

class Date
{
    friend ostream& operator<<(ostream& _cout, const Date& d);
public:
    Date(int year, int month, int day): _year(year), _month(month), _day(day)//初始化列表
    {}
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d(2023, 8, 15);
    Date d1(2023, 8, 16);
    cout << d;
    cout << d1;
    cout << d << d1;
    return 0;
}
ostream& operator<<(ostream& _cout,const Date &d)//->operator<<(Date *this,ostream& _cout)隐含的this指针被规定在参数最左侧导致调用无法反过来
{
    _cout << d._year << "-" << d._month << "-" << d._day << endl;
    return _cout;
}

1.2友元类

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

友元关系是单向的,不具有交换性。 

友元关系不能传递。

        如果C是B的友元, B是A的友元,则不能说明C时A的友元。

友元关系不能继承。 

class Time
{
    friend class Date;   // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
    Time(int hour = 0, int minute = 0, int second = 0)
        : _hour(hour)
        , _minute(minute)
        , _second(second)
    {}

private:
    int _hour;
    int _minute;
    int _second;
};
class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
        : _year(year)
        , _month(month)
        , _day(day)
    {}

    void SetTimeOfDate(int hour, int minute, int second)
    {
        // 直接访问时间类私有的成员变量
        _t._hour = hour;
        _t._minute = minute;
        _t._second = second;
    }

private:
    int _year;
    int _month;
    int _day;
    Time _t;
};

2.再谈构造函数

2.1构造函数体赋值和初始化列表

构造函数体赋值:在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟 一个放在括号中的初始值或表达式。

构造函数体赋值为何不能叫做初始化?

       构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值

为何引入初始化列表(对比构造函数体赋值的特性升级)

        C++引入初始化列表的主要目的是在对象的构造过程中初始化成员变量或基类。它提供了在构造函数中对成员变量或基类进行初始化的一种便捷、高效的方式。

1.每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)。

2.类中包含以下成员,必须放在初始化列表位置进行初始化:

       初始化常量成员变量:类中的常量成员变量在对象创建后不能被修改,因此需要进行初始化。所以在构造函数中使用初始化列表可以直接对常量成员变量进行初始化。

       初始化引用成员变量:引用成员变量必须在创建对象时进行初始化,并且一旦初始化,它们将一直引用同一个对象。初始化列表提供了在构造函数中对引用成员变量进行初始化的方式。

       初始化非默认构造函数的类成员变量:如果类成员变量没有默认构造函数,或者我们希望使用特定的参数初始化它们,那么初始化列表就可以提供这种初始化的方式。

      初始化基类:派生类的构造函数在创建对象时需要先调用基类的构造函数,通过初始化列表可以指定要调用的基类构造函数,并提供参数。

3.不论是否使用初始化列表,对于自定义类型成员变量, 一定会先使用初始化列表初始化,再使用函数体内部的赋值函数进行赋值。

4.成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后 次序无关

 

2.2初始化列表和构造函数体赋值混合使用

      对于某些特定的场景,只是初始化列表往往不能满足我们的需求,比如,类成员中包含数组,我们可以用初始化列表来分配空间,此时,如果我们需要对分配的数组空间赋初值,就需要在函数体内部实现了。

下面是数组模拟栈构造函数的用例: 

2.3 explicit关键字

C++11的隐式类型转换

class A
{
public :
	A(int a):_a(a)
	{
		cout << "A(int a)" << endl;
	}
	A(const A& a):_a(a._a)
	{
		cout << "A(const A& a)" << endl;
	}
private:
	int _a=0;//缺省值
};

class B
{
public:
	B(int b1,int b2) :_b1(b1),_b2(b2)
	{
		cout << "B(int b1,int b2)" << endl;
	}
	B(const B& b) :_b1(b._b1), _b2(b._b2)
	{
		cout << "B(const B& b)" << endl;
	}
private:
	int _b1 = 0;//缺省值
	int _b2;
};

int main()
{
	A a1(1);
	A a2 = 2;//单参数的隐式类型转换,用2调用A的构造函数生成一个临时对象,再用这个对象调用拷贝构造生成a2
	         //编译器存在优化,优化成用2直接构造,也就是不会再单独调用一个拷贝构造
	//A& res = 3;//中间会生成临时变量,而临时变量具有常性,所以,引用必须加const
	const A& res = 3;

	//多参数
	B b1(1, 1);
	B b2 = {2,2};//多参数的隐式转换
	const B& ans = { 3,3 };

	return 0;
}

      构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用,用explicit修饰构造函数,将会禁止构造函数的隐式转换

class Date
{
public:
	// 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用
	 // explicit修饰构造函数,禁止类型转换---explicit去掉之后,代码可以通过编译
	explicit Date(int year)
		:_year(year)
	{}
	/*
	// 2. 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具
   有类型转换作用
	// explicit修饰构造函数,禁止类型转换
	explicit Date(int year, int month = 1, int day = 1)
	: _year(year)
	, _month(month)
	, _day(day)
	{}
	*/
	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;
};
void Test()
{
	Date d1(2022);
	// 用一个整形变量给日期类型对象赋值
	// 实际编译器背后会用2023构造一个无名对象,最后用无名对象给d1对象进行赋值
	d1 = 2023;
	// 将1屏蔽掉,2放开时则编译失败,因为explicit修饰构造函数,禁止了单参构造函数类型转
	换的作用
}

3.static成员

     声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用 static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。

特性:

1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区

2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明

3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员来访问

4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员

5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制

面试题:实现一个类,计算程序中创建出了多少个类对象。

class A
{
public:
	A() { ++_scount; }
	A(const A& t) { ++_scount; }
	~A() { --_scount; }
	static int GetACount() { return _scount;}
private:
	static int _scount;
};
int A::_scount = 0;
void TestA()
{
	cout << A::GetACount() << endl;
	A a1,a2;
	A a3(a1);
	cout << A::GetACount() << endl;
}

静态成员函数与非静态成员函数

       静态成员函数是与类关联而不是与类的实例关联的函数。它们不依赖于任何特定的对象实例,因此可以在没有对象实例的情况下被调用。静态成员函数可以通过类名直接调用,而不需要创建类的对象

       非静态成员函数是与类的实例关联的函数。它们依赖于特定的对象实例,并且可以访问和操作对象的非静态成员变量和其他非静态成员函数。

      静态成员函数可以调用非静态成员函数,由于静态成员函数不依赖于对象实例,它们无法直接访问非静态成员变量和非静态成员函数。但是,静态成员函数可以通过对象实例或类的引用/指针来调用非静态成员函数。在静态成员函数内部,可以创建对象实例或使用类的引用/指针来调用非静态成员函数。

     非静态成员函数可以直接调用类的静态成员函数 ,非静态成员函数可以直接访问和调用类的静态成员变量和静态成员函数,因为它们与对象实例关联,并且可以通过对象实例或类的引用/指针来访问类的静态成员。

4.匿名对象

      在C++中,匿名对象是指在创建对象时没有为其分配一个具名的变量,而是直接使用对象进行操作或传递给函数。匿名对象的生命周期仅限于当前语句,一旦语句执行完毕,匿名对象将被销毁

匿名对象通常应用于以下的几种情况:

1.作为函数的返回值:可以将匿名对象作为函数的返回值,从而避免创建额外的变量。例如:

class MyClass {
public:
    int getValue() {
        return value;
    }

private:
    int value = 42;
};

MyClass createObject() {
    return MyClass(); // 返回匿名对象
}

int main() {
    int result = createObject().getValue(); // 使用匿名对象调用函数并获取值
    return 0;
}

2.作为函数参数:可以将匿名对象作为函数的参数传递,从而避免创建额外的变量。例如:

class MyClass {
public:
    void printValue() {
        std::cout << value << std::endl;
    }

private:
    int value = 42;
};

void printObject(MyClass obj) {
    obj.printValue();
}

int main() {
    printObject(MyClass()); // 将匿名对象作为参数传递给函数
    return 0;
}

3.临时对象的操作:可以直接对匿名对象进行操作,而无需为其分配一个具名的变量。例如:

class MyClass {
public:
    void printValue() {
        std::cout << value << std::endl;
    }

private:
    int value = 42;
};

int main() {
    MyClass().printValue(); // 对匿名对象调用成员函数
    return 0;
}

     由于匿名对象的生命周期短暂且无法被其他代码引用,因此在使用匿名对象时需要确保不会产生悬空引用或内存泄漏的问题,使用时应当注意,匿名对象一般操作中并不常见,了解即可。

5.拷贝对象时的一些编译器优化

     在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还是非常有用的,这里我们也是做了解即可,不需要深究。

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a)" << endl;
	}
	A(const A& aa)
		:_a(aa._a)
	{
		cout << "A(const A& aa)" << endl;
	}
	A& operator=(const A& aa)
	{
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
			_a = aa._a;
		}
		return *this;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};
void f1(A aa)
{}
A f2()
{
	A aa;
	return aa;
}
int main()
{
	// 传值传参
	A aa1;
	f1(aa1);
	cout << endl;
	// 传值返回
	f2();
	cout << endl;
	// 隐式类型,连续构造+拷贝构造->优化为直接构造
	f1(1);
	// 一个表达式中,连续构造+拷贝构造->优化为一个构造
	f1(A(2));
	cout << endl;
	// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
	A aa2 = f2();
	cout << endl;
	// 一个表达式中,连续拷贝构造+赋值重载->无法优化
	aa1 = f2();
	cout << endl;
	return 0;
}

   

      如果你真的想要逆风翻盘,那你就要踏上痛苦的自律,走上枯燥的学习之路,即使基础不好也要学,学到哭,学到崩溃也要继续。桌子上的不只是书,是你的前途和父母暮年时的欢喜。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/904503.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一个模型解决所有类别的异常检测

文章目录 一、内容说明二、相关链接三、概述四、摘要1、现有方法存在的问题2、方案3、效果 五、作者的实验六、如何训练自己的数据1、数据准备2、修改配置文件3、代码优化修改4、模型训练与测试 七、结束 一、内容说明 在我接触的缺陷检测项目中&#xff0c;检测缺陷有两种方法…

TMS320C54X 的软件编程

1、DSP 编程工具与流程 DSP 的设计目标是进行数字信号处理&#xff0c;在硬件设计的基础上选择好一定的优化算法并 通过编程在 DSP 芯片上实现是 DSP 技术的核心内容。对 DSP 进行编程&#xff0c;目前最有效的语言 工具仍是 DSP 汇编语言&#xff0c;同时为方便用户用高级语言…

每日一题之数值的整数次方

数值的整数次方 描述&#xff1a; 实现函数 double Power(double base, int exponent)&#xff0c;求 base 的 exponent 次方。 注意&#xff1a; 1.保证base和exponent不同时为0。 2.不得使用库函数&#xff0c;同时不需要考虑大数问题 3.有特殊判题&#xff0c;不用考虑小数…

mysql+jdbc+servlet+java实现的学生在校疫情信息打卡系统

摘 要 I Abstract II 主 要 符 号 表 i 1 绪论 1 1.1 研究背景 1 1.2 研究目的与意义 2 1.3 国内外的研究情况 2 1.4 研究内容 2 2 系统的开发方法和关键技术 4 2.1 开发方法 4 2.1.1 结构化开发方法 4 2.1.2 面向对象方法 4 2.2 开发技术 4 2.2.1 小程序开发MINA框架 4 2.2.2 …

[Spring]事务相关

事务隔离级别 Spring 提供了以下五种事务隔离级别&#xff0c;用于控制多个事务之间的相互影响&#xff1a; DEFAULT&#xff08;默认隔离级别&#xff09;&#xff1a; 使用底层数据库的默认隔离级别。通常为数据库的默认隔离级别&#xff0c;如 MySQL 是 REPEATABLE READ&am…

逆向退货售后设计

一&#xff0c;项目背景 退货售后流程&#xff1a;涉及订单&#xff0c;票&#xff0c;款 二&#xff0c;方案一 三&#xff0c;方案二

残差网络实现

代码中涉及的图片实验数据下载地址&#xff1a;https://download.csdn.net/download/m0_37567738/88235543?spm1001.2014.3001.5501 代码&#xff1a; import torch import torch.nn as nn import torch.nn.functional as F #from utils import load_data,get_accur,train i…

只需5分钟,了解常见的四种限流算法

一、计数器算法 在指定周期内累加访问次数&#xff0c;当访问次数达到设定的阈值时&#xff0c;触发限流策略&#xff0c;当进入下一个时间周期时进行访问次数的清零。如图所示&#xff0c;我们要求3秒内的请求不要超过150次&#xff1a; 但是&#xff0c;貌似看似很“完美”的…

[mysql系列] mysql 数据库如何实现事务回滚

这里写自定义目录标题 一、事务回滚二、mysql InnoDB引擎如何实现回滚操作2.1 InnoDB引擎的 undo log2.2 具体实现2.2.1 insert 操作2.2.2 delete 操作2.2.3 update 操作 主要参考资料为&#xff1a;《Mysql 是怎样运行的》 一、事务回滚 根据原子性的定义&#xff0c;一个事务…

MySQL MVCC的详解之Read View

文章目录 概要一、基于UNDO LOG的版本链1.1、行记录结构1.2、了解UNDO LOG1.3、版本链 二、Read View2.1、判定机制 三、参考 概要 在上文中&#xff0c;我们提到了MVCC&#xff08;Multi-Version Concurrency Control)多版本并发控制&#xff0c;是通过undo log来实现的。那具…

MATLAB | 七夕节用MATLAB画个玫瑰花束叭

Hey又是一年七夕节要到了&#xff0c;每年一次直男审美MATLAB绘图大赛开始hiahiahia&#xff0c;真的这些代码越写越不知道咋写&#xff0c;又不想每年把之前的代码翻出来再发一遍&#xff0c;于是今年又对我之前写的老代码进行了点优化组合&#xff0c;整了个花球变花束&#…

计算机网络-物理层(三)-信道的极限容量

计算机网络-物理层(三)-信道的极限容量 当信号在信道中传输失真不严重时&#xff0c;在信道的输出端&#xff0c;这些信号可以被识别 当信号在信道中&#xff0c;传输失真严重时&#xff0c;在信道的输出端就难以识别 造成失真的因素 码元传输速率信号传输距离噪声干扰传输媒…

8.4.tensorRT高级(3)封装系列-infer推理封装,输入输出tensor的关联

目录 前言1. infer封装总结 前言 杜老师推出的 tensorRT从零起步高性能部署 课程&#xff0c;之前有看过一遍&#xff0c;但是没有做笔记&#xff0c;很多东西也忘了。这次重新撸一遍&#xff0c;顺便记记笔记。 本次课程学习 tensorRT 高级-infer推理封装&#xff0c;输入输出…

思维进化算法(MEA)优化BP神经网络

随着计算机科学的发展,人们借助适者生存这一进化规则,将计算机科学和生物进化结合起来,逐渐发展形成一类启发式随机搜索算法,这类算法被称为进化算法(Evolutionary Com-putation, EC)。最著名的进化算法有:遗传算法、进化策略、进化规划。与传统算法相比,进化算法的特点是群体搜…

嵌入式设备应用开发(boost库应用)

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】 嵌入式开发过程中不可避免在很多情况下,需要使用到posix的api函数。一方面,这些api函数确实可以帮助我们解决一些问题;但是另外一方面,因为平台的差异,如果一段时间不做嵌入式…

Java集合利器 Map Set

Map & Set 一、概念二、Map三、Set下期预告 一、概念 Map和Set是一种专门用来进行搜索的数据结构&#xff0c;其搜索的效率与其具体的实例化子类有关。它们分别定义了两种不同的数据结构和特点&#xff1a; Map&#xff08;映射&#xff09; &#xff1a;Map是一种键值对&…

Spring高手之路13——BeanFactoryPostProcessor与BeanDefinitionRegistryPostProcessor解析

文章目录 1. BeanFactoryPostProcessor 概览1.1 解读 BeanFactoryPostProcessor1.2. 如何使用 BeanFactoryPostProcessor 2. BeanDefinitionRegistryPostProcessor 深入探究2.1 解读 BeanDefinitionRegistryPostProcessor2.2 BeanDefinitionRegistryPostProcessor 的执行时机2.…

Redis+SpringBoot企业版集群实战------【华为云版】

目录 安装 复制及集群 bgsave rdb aof SpringBoot+Redis操作

斯特林近似(Stirling‘s approximation)

斯特林公式&#xff08;Stirling’s approximation&#xff09;是一条用来取n的阶乘的近似值的数学公式。一般来说&#xff0c;阶乘的计算复杂度为线性。当要为某些极大的n求阶乘时&#xff0c;常见的方法复杂度不可接受。斯特林公式能够将求解阶乘的复杂度降低到对数级。而且&…

在云服务器上安装Jenkins

说明&#xff1a;Jenkins是一个部署项目的平台&#xff0c;通过Jenkins可以省去从项目开发–>部署项目之间的所有流程&#xff0c;做到代码提交即上线。本文介绍在云服务CentOS上安装Jenkins。 前提 安装Jenkins之前&#xff0c;先要在云服务上安装JDK、Maven、Git&#x…