C++经典问题_11 C++类默认创建的成员函数

一. 创建一个空类的时候编译器会自动生成哪些函数

① 默认构造函数
② 默认的拷贝构造函数
③ 默认的析构函数
④ 默认的重载赋值运算符函数
⑤ 默认的重载取地址运算符函数
⑥ 默认的重载取地址运算符const函数
⑦ 默认移动构造函数(C++11)
⑧ 默认重载移动赋值操作符函数(C++11)

只声明一个空类,编译器会自动创建上述的函数,当然这些函数只有在第一次被调用的时候,才会被编译器创建.如果我们不希望对象被显示的构造或者赋值,可以将对应的函数声明为private,或者写一个基类,开放部分默认函数,子类去继承就可以了.

defalut: 被标识的默认函数将使用类的默认行为,如: A() = default;
delete:被标识的默认函数将禁用,如: A() = delete;
override:被标识的函数需要强制重写基类虚函数
final:被标识的函数将禁止重写基类虚函数

二. 默认的成员函数解析

① 默认构造函数

原型

class Person
{
	Person()
	{
		// 默认的构造函数,又称为缺省构造,什么都不做.
	}
};

特点

  1. 当程序员没有定义任何的构造的函数的时候,编译器在需要的时候会生成一个默认的构造函数.
  2. 一旦程序员定义了构造函数,缺省的默认构造函数都不会再创建,所以一般建议程序员自己创建一个缺省的默认构造.因为这种缺省默认构造有时候是很有用的.
② 默认的拷贝构造函数

原型

class Person
{
	// 默认的拷贝构造函数
	Person(const Person& p)
	{
		// 默认的拷贝构造函数做的事情就是浅拷贝,将所有的数据成员一一赋值
		mA = p.mA;
		mB = p.mB;
	}
private:
	int mA;
	int mB;
}

调用时机

  1. 使用已经存在的对象进行初始化,会调用拷贝构造函数.
  2. 创建对象的时候直接使用另外的对象进行赋值(本质上还是和1的调用一样)
  3. 当对象作为实参,形参是类类型的时候,会调用拷贝构造.注意形参不能是引用类型
  4. 当函数的返回值是类类型的时候,会调用拷贝构造.注意返回值不能是引用类型
/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/

#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
	Person(string name, int age) :mName(name), mAge(age)
	{
		cout << "有参构造被调用!" << endl;
	}

	Person(const Person &p)
	{
		cout << "默认的拷贝构造函数被调用!" << endl;
		mName = p.mName;
		mAge = p.mAge;

	}
private:
	string mName;
	int mAge;
};

void test_01(Person p)
{
	cout << "test_01() 被调用!" << endl;
}

void test_02(Person& p)
{
	cout << "test_02() 被调用!" << endl;
}


Person test_03(void)
{
	Person p("Fioman", 28);
	return p;
}

Person &test_04(void)
{
	Person p("Fioman", 18);
	Person &p1 = p;
	return p1;
}
int main()
{
	// 1. 用已经存在的对象进行初始化,会调用拷贝构造函数
	Person p1("张三", 18);
	Person p2(p1);

	// 2. 创建对象的时候直接用另外的对象赋值
	Person p3 = p1; // 其实和Person p3(p1) 是等价的,这里调用的不是赋值操作符函数

	// 如果是下面这种,就不是调用的拷贝构造函数
	Person p4("李四",3);
	p4 = p1; //这里调用的是赋值操作符函数

	// 3. 对象作为实参,形参是类对象,注意不是类对象的引用
	test_01(p1);

	// 如果形参是引用类型,是不会调用拷贝构造函数的
	test_02(p1);

	// 4. 函数的返回类型是类类型的时候,注意返回类型不能是引用类型
	test_03();

	// 5. 如果返回类型是引用类型的时候,不会调用拷贝构造函数
	test_04();

	system("pause");
	return 0;
}

结果:

③ 默认的析构函数

原型

class Person
{
public:
	~Person()
	{
		// 默认的析构函数
	}
};

析构函数调用的时机
无论何时一个对象被销毁,就会自动调用其析构函数:

  1. 变量在离开其作用域时被销毁
  2. 当一个对象被销毁时,其成员被销毁
  3. 容器(标准库容易或者数组)被销毁时,其元素被销毁
  4. 对于动态分配的对象,当对指向它的指针使用delete运算符时被销毁
  5. 临时对象,当创建它的完整表达式结束时被销毁

默认的析构函数的作用

析构函数体执行完毕后,成员会被自动销毁.但是默认的析构函数本身不会直接销毁成员.成员是在析构函数体之后隐含的析构阶段中销毁的.在整个对象销毁的过程中,析构函数体是作为成员销毁步骤之外的另一部分进行的.

④ 默认的赋值运算符函数

原型

class Person
{
	Person(string name, int age) :mName(name), mAge(age)
	{
		cout << "Person() 的有参构造被调用!" << endl;
	}

	Person &operator=(const Person &p)
	{
		cout << "Person() 的默认的赋值运算符函数被调用!" << endl;
		// 对成员属性进行简单的赋值操作
		this->mName = p.mName;
		this->mAge = p.mAge;
		return *this;
	}
private:
	string mName;
	int mAge;
};

调用时机

  1. 当为一个类对象进行赋值的时候,会调用该类对象的赋值运算符函数.
  2. 注意一点的就是不是有=号出现的地方就是进行赋值操作
Person p; // 对象声明
Person p2; // 对象声明
p2 = p; // 对象赋值
Person p3 = p2; // 这里是对象初始化,虽然有赋值符号,但是这里不会调用赋值运算符函数,而是调用拷贝构造函数

什么情况下编译器会提供一个默认的赋值运算符函数

当程序没有显示地提供一个以本类或者本类的引用作为参数的赋值运算符函数的时候,编译器会自动生成一个赋值运算符函数.默认的重载函数不创建时有条件的,不是说你随便创建了一个赋值重载运算符就行了,参数要匹配上,默认的意思应该理解成以本类或者本类的引用作为参数

/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/

#include <iostream>
using namespace std;
class Person
{
public:
	Person()
	{
		cout << "Person() 的默认构造函数被调用!" << endl;
	}
	Person(int age):mAge(age)
	{
		cout << "Person() 的有参构造被调用!" << endl;
	}

	Person &operator=(const int a)
	{
		cout << "重载的非类类型的赋值运算符函数" << endl;
		this->mAge = a;
		return *this;
	}
private:
	int mAge;
};

int main()
{
	Person p1(12);
	Person p2;
	p2 = p1; // 这里调用的不是我们自己定义的赋值运算符函数,而是系统定义的默认的赋值运算符函数
	cout << "==============================================" << endl;
	p2 = 10; // 这里调用的是自己定义的非类类型的赋值运算符函数
	system("pause");
	return 0;
}

结果:

如果我们自己定义了类类型或者是类的引用类型作为参数的赋值运算符函数,那么编译器就不会再提供默认的赋值运算符函数

参数说明

  1. 一般地,赋值运算符重载函数的参数是函数所在类的const的引用
  2. const的目的是我们不希望这个函数中对用来进行赋值的原来的那个对象做任何的修改
  3. 加上const,对于cosnt的和非const的实参函数都能接受,如果不加,就只能接受非const实参
  4. 使用引用是因为避免在函数的调用过程中对实参再进行一次拷贝,提高了效率

上面的这些说明不是强制的,可以不加const,也可以没有引用,甚至参数可以不是函数所在的对象.

返回值

  1. 返回值是被赋值者的引用,即*this
  2. 返回时避免了拷贝,提高了效率
  3. 可以实现连续赋值,即类似a=b=c.

这也不是强制的,可以返回void类型,只是这样做之后,无法实现我们平时使用赋值操作符时候的一些美好的特性.

⑤ 默认的普通取地址操作符

原型

class Person
{
	Person *operator&()
	{
		// 取地址运算符,一板就是返回这个对象的本身的地址,一般不需要程序员进行重载.
		return this;
	}
};

注意事项

  1. 取地址操作符是一个单目运算符,一般就是获取当前对象的地址,使用this即可,不需要传参.
  2. 取地址操作符一般不需要重载,但是需要创建一个常函数版本的取地址操作符,可以让常对象在取地址的时候调用
⑥ const属性的取地址操作符

原型

/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/

#include <iostream>
using namespace std;
class Person
{
public:
	Person *operator&()
	{
		// 取地址运算符,一板就是返回这个对象的本身的地址,一般不需要程序员进行重载.
		cout << "operator&() 普通的被调用!" << endl;
		return this;
	}

	const Person *operator&() const
	{
		cout << "operator&() const 被调用!" << endl;
		return this;
	}
};

int main()
{
	Person p;
	cout << "对象p的地址: " << &p << endl;
	cout << "=================================" << endl;
	const Person p1;
	cout << "常对象p的地址: " << &p1 << endl;
	system("pause");
	return 0;
}

结果:

⑦ 默认的移动构造函数

对象移动

  1. 为了解决对象拷贝带来的资源开销,引出了对象移动的功能
  2. 在一些情况下,如果对象拷贝之后就被销毁了,这个时候使用对象移动比拷贝可以提高很大的性能.
  3. 在旧版本的标准库中,容器中所保存的类必须是可拷贝的,但是新标准中,我们可以用容器保存不可拷贝的类型,只要它们可以被移动即可.

右值引用

  1. 普通的引用,我们可以称之为左值引用,左值引用不能将其绑定到表达式,字面值常量或者是返回右值的表达式.
  2. 而右值引用就是必须绑定到右值的引用.我们通过&&来获取右值引用.右值引用有一个特性,只能绑定到一个将要销毁的对象.
  3. 右值引用也不过是某个对象的另一个名字而已,但是右值引用不能绑定到一个左值上面.
  4. 什么是左值? 就是可以用取地址操作符操作的变量都是左值,其他的就是右值.
int i = 42; // i是左值 42是右值
int &r = i; // 正确,普通的引用引用一个左值i
int &&rr = i; // 错误,不能将一个右值引用绑定到一个左值上
int &r2 = i * 42; // 错误,i * 42 是一个右值,不能将普通的引用绑定到一个表达式上面
const int &r3 = i * 42; // 正确,我们可以将一个const的引用绑定到一个右值上面
int && rr2 = i * 42; // 正确,可以将右值引用绑定到表达式上面

总结:

  1. 返回左值引用的函数,连同赋值,下标,解引用和前置递增/递减运算符,都是返回左值表达式的例子.将一个左值引用可以绑定到这类表达式的结果上
  2. 返回非引用类型的函数,连同算术,关系,位,以及后置递增/递减运算符,都是生成右值.我们不能将左值引用绑定到这类表达式上,但是可以静一个const的左值引用或者是右值引用绑定到该表达式上.
  3. 左值持久,右值短暂. 右值要不是字面值常量,要不就是表达式求值过程中创建的临时对象.

右值引用特点:

  1. 所引用的对象将要被销毁
  2. 该对象没有其他用户
  3. 使用右值引用的代码可以自由地接管所引用的对象的资源.

什么情况下编译器会创建默认的移动构造函数

  1. 如果编译器提供了拷贝构造函数,拷贝赋值运算符,或者是析构函数,编译器将不会创建默认的移动构造函数
  2. 如果一个类没有移动操作,那么类会使用对应的拷贝操作来代替移动操作.
  3. 只有当一个类没有定义任何版本的拷贝构造函数,并且类的每个非static数据成员是可移动的时候,编译器才会创建默认的移动构造函数.内置的类型成员是可移动的.
class A
{
	int i; // 内置类型可以移动
	string s;	// string定义了自己的移动操作
};

class B
{
	A a; // A有默认的移动操作
}

A a;
A a1 = std::move(a); // 使用默认的移动构造函数
B b;
B b2 = std:move(b); // 使用默认的移动构造函数

如果没有移动构造函数,右值也会被拷贝

如果一个类有一个拷贝构造函数,但是未定义移动构造函数,编译器是不会创建默认的移动构造函数的,它会调用拷贝构造函数,即使你使用move也是一样的效果.

/*----------------------------------------------------------------
* 项目: Classical Question
* 作者: Fioman
* 邮箱: geym@hengdingzhineng.com
* 时间: 2022/3/22
* 格言: Talk is cheap,show me the code ^_^
//----------------------------------------------------------------*/

#include <iostream>
using namespace std;
class Person
{
public:
	Person()
	{
		cout << "Person() 的缺省构造函数被调用!" << endl;
	}

	Person(const Person &p)
	{
		cout << "Person() 的拷贝构造函数被调用!" << endl;
	}


};

int main()
{

	Person p1;
	Person p2(p1); // 拷贝构造函数,p1是一个左值
	Person p3(std::move(p1)); // 拷贝构造函数,虽然是右值,但是未定义移动构造函数,但是定义了拷贝构造函数

	system("pause");
	return 0;
}

结果:

⑧ 移动赋值运算符

原型

Person & operator=(Person && p) noexcept{};

移动赋值运算符和移动构造函数一样,如果一旦用户创建了拷贝构造函数,就不再提供默认的移动赋值运算符了.

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
st_asio_wrapper是一组,功能是对boost.asio的包装(调试环境:boost-1.51.0),目的是简化boost.asio开发; 其特点是效率高、跨平台、完全异步,当然这是从boost.asio继承而来; 自动重连,数据透明传输,自动解决分包粘包问题(你可以像udp一样使用它); 注:只支持tcp协议; 教程:http://blog.csdn.net/yang79tao/article/details/7724514 1.1版更新内容: 增加了自定义数据模式的支持,可用于st_asio_wrapper server与其它客户端的通信、或者st_asio_wrapper client与其它服务端的通信;当然,两端都是st_asio_wrapper的话,就用透明传输即可(1.0版已经支持了)。 1.2版更新内容: 修复BUG:当stop_service之后,再start_service时,client_base内部某些成员变量可能没有得到复位; 服务端增加修改监听地址功能,当然仍然要在start_service之前调用set_server_addr函数。 1.3版更新内容: 增加自定义消息格式的发送,这个本来是在1.1版本实现的,结果我漏掉了,只实现了自定义消息格式的接收。 1.4版更新内容: 将打包与解包器从client_base分离出来,以简化这个日益复杂的基; 可以在运行时修改打包解包器。 1.5版更新内容: 增加ipv6支持,默认是ipv4,服务端和客户端都通过设置一个ipv6的地址来开启这个功能; 增加了一些服务端helper函数,小改了一下客户端set_server_addr函数签名(调换了两个参数的位置以保持和服务端一样)。 1.6版更新内容: 增加了接收消息缓存(改动较大,on_msg的语义有所变化,请看开发教程第三篇)。 1.7版更新内容: 修复vc2010下编译错误; 修复默认解包器BUG(同时修改解包器接口); 修复log输出BUG; 更好的包装了服务端库,现在服务端可以像客户端一样简单的使用了(完全不用继承或者重写虚函数,申请一个对象即可); 结构大调整,名大调整,请参看开发教程第一篇。 1.8版更新内容: 增加健壮性和稳定性; 退出服务更新优雅。 1.9版更新内容: 提高代码通用性; 可以指定服务端同时投递多少个async_accept; 修复BUG,此BUG可能造成数据发送不完全。 2.0版更新内容: 服务端增加对象池功能; 优化美化代码; 更规范化接口签名。 2.1版更新内容: 修复BUG,此BUG会造成st_client在stop_service之后,仍然可能尝试重新连接服务器; 在消息发送的时候,增加了一个参数can_overflow,用于确定是否在缓存满的时候返回失败,这在某些不能阻塞等待直到缓存可用的场合非常有用,比如on_msg; 当消息接收缓存满的时候,st_socket现在可以保证消息不丢失,之前的行为是调用on_recv_buffer_oveflow之后,丢弃消息; 更规范化接口签名; 更多更新请看st_asio_wrapper_socket.h,所有更新都会罗列在这个头文件的开头处,另外st_asio_wrapper_server.h的开头部分注释也很重要,有工作原理相关的说明。 2.2版更新内容: 增加了一个demo——文件传输服务端及客户端,它几乎可以当成软件来使用,多线程且支持分块传送; 增加了timer功能,考虑到st_server已经用上了timer,加之做文件传输服务器的时候,也用到了timer,所以干脆把timer抽象出来,其设计原理及使用跟MFC的timer差不多。timer已经被st_socket和st_server继承。用的时候,调用set_timer开始timer,重写on_timer虚函数等待被调用即可; 进一步优化消息接收缓存,通过宏,让优化发生于编译阶段,参看FORCE_TO_USE_MSG_RECV_BUFFER宏以了解更多。demo里面有使用案例(asio_server,asio_client,file_server,file_client,performance/asio_client); 增加了所有的宏都可以在外面修改的功能,在include库头文件之前,可以定义你想要的宏,st_asio_wrapper不会修改它们,除非它们没有被定义,此时使用默认值,这也是st_asio_wrapper之前设计不好的地方,具体请参看使用教程。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值