C++新标准,查漏补缺(1)基础

6 篇文章 0 订阅


本文是读《C++ Primer》(第五版)的笔记,但并不是事无巨细的笔记,而是一个C++98的老鸟,学习C++11,依据以往经验实践,对新标准的一种体会。
本文不面向新手,一方面给自己看,一方面希望有同样背景的C++开发人员能有所得,包含大量案例,如有不足,欢迎指正,共同学习。
C++新标准,查漏补缺(1)基础
C++新标准,查漏补缺(2)标准库
C++新标准,查漏补缺(3)类设计者的工具
C++新标准,查漏补缺(4)高级主题

基本内置类型

1. 泛化的转义序列

  1. 常规转义字符如 ‘\n’ 对应ascii码 10
  2. 泛化转义序列,可以通过 ‘\’ + 1/2/3个八进制数字,表示对应ascii码值,表示一个字节,如:\n 可以表示为 ‘\12’
  3. 泛化转义序列,可以通过 ‘\x’ + 1/2 个十六进制数字,表示对应ascii码值,表示一个字节,如:\n 可以表示为 ‘\x0A’
    参考代码:
int main()
{
    int num{ '\n' };
    std::cout << num << std::endl;
    std::cout << int{ '\n' } << std::endl;		//< 输出:10
    std::cout << int{ '\12' } << std::endl;		//< 输出:10
    std::cout << int{ '\x0A' } << std::endl;	//< 输出:10
    system("pause");
    return 0;
}

2. 指定字符字面值

  1. 常规的如:L"12345",通过L指定该字符常量为wchar_t类型
    wchar_t类型与另一种整型(底层类型,不是指int)的长度和符号属性相同,对具体整型的选择取决于系统实现。因此在一个系统中,它可能是unsigned short,而在另一个系统中,则可能是int。
    Windows中:sizeof(L"12345") = 12个字节(末尾\0\0)
  2. u‘12345’,类型char16_t,表示Unicode16,每个字符2个字节,共12字节(包括"\0\0")
  3. U’12345’,类型char32_t,表示Unicode32,每个字符4个字节,共24字节(包括“\0\0\0\0”)
  4. 【关键好用】u8“12345”,类型char,使用utf-8,每个字符串ascii码下为1字节,其他如中文编码采用不定长字节,以\0结尾。如:u8"中",为4个字节(“中”3个字节 + '\0’1个字节)。
    在Windows 尤其qt编程时,使用u8定义字符常量,可以避免将cpp文件的编码格式对字符串编码的影响。
    由于u8是char类型变量,还可以直接存储在QString/string中,使用常规字符串函数处理,避免u16/u32/wchart_t的转换,好处很多。

初始化

1. 列表初始化

初始化的语法较特殊,和赋值的语法完全两码事
特性:使用花括号“{}”对变量进行初始化,不限定变量类型
限制:当对内建类型初始化时,如果编译检测有数据丢失风险,会报错,如

int a{0.12f}; \\< 编译错误

报错

1>E:\01.study\C++\C++11\code\TestCpp11\TestCpp11\TestCpp11.cpp(10,24): error C2397: 从“float”转换到“int”需要收缩转换

2. 列表初始化-补充

列表初始的定义:
{ initializer-list }:初始化列表,可嵌套使用,如,{ {1, 2}, {3, 4} }
{ designated-initializer-list }(C++20):仅适用于结构体、聚合类型,可指定元素初始化,如,{.m=2, .n=3}
这里就引申出来,对自定义类型,如何进行列表初始?如下:

class CTestConstruct
{
public:
	CTestConstruct(int nNum, std::string strMsg)
		: m_nNum(nNum)
		, m_strMsg(strMsg)
	{
		printf("%s,%d: msg,%s, num,%d\r\n",
			__FUNCTION__, __LINE__, m_strMsg.c_str(), m_nNum);
	}
private:
	int m_nNum;
	std::string m_strMsg;
};

int main()
{
	CTestConstruct obj{ 3, "33" };
	CTestConstruct arrObj[2] = { {1,"11"}, {2, "22"} }; //< 相当对数组每个元素,再执行列表初始化

	typedef struct
	{
		int x;
		int y;
	}STU_DATA;
	STU_DATA stdData{ .x = 4, .y = 44 };
	system("pause");
	return 0;
}

复合类型

1. 空指针

定义:空指针不指向任何对象,其值 = 0,C++新标准中使用 nullptr表示

int *p1 = nullptr; //< 等价于 int *p1 = 0;
int *p2 = 0;
int *p3 = NULL;

注意:尽量避免使用NULL,这个值属于预定义变量而并非关键字,其值在不同系统中可能不同

2. 二维数组和二维指针

二维数组
C++语言中并没有真正的多维数组,所谓的多维数组,实际是数组的数组
如下定义

int a[][3] = {{1, 2, 3}, {2, 3, 4}}

a 首元素的本质是一个一维数组,即一维指针,数组元素的类型 是int[3]。
二维数组的第二维是数组元素类型的一部分,所以不能省略。
以上定义等价于

typedef arr_item int[3];
arr_item *a[] = {{1, 2, 3}, {2, 3, 4}};

由于a是一维数组,数组元素大小为 sizeof(int[3]),步长为3,大小12个字节。
其内存结构是连续的,如下
a的本质是一维数组
二维指针
二维指针的定义是指向指针的指针,和二维数组(本质是一维指针)存在本质的不同。
如下定义

	int** pA = new int*[2];
    pA[0] = new int[3];
    pA[1] = new int[3];

pA是一个二维指针,指向一个指针数组。
数组元素大小为sizeof(int *),步长为1,64位下大小4个字节。
第二维的指针不连续,内存结构如下
在这里插入图片描述

const限定

1. const引用

被称为万能引用
如:

int i = 10;
const int ci = 100;
const int &i1 = i;
const int &i2 = 10;
const int &i3 = ci;

这个在函数参数中就很有用了

int func(const int &param);
int func2(int &param);

int i = 10;
const int ci = 100;
func2(i);
func2(10);		//< 编译错误,使用常量
func2(ci);		//< 编译错误,使用常量

//! 而以下,都能编译通过
func(i);
func(100);
func(ci);

2. constexpr常量表达式

constexpr用于修饰一个常量表达式(必须在编译阶段被识别),在以下情况可修饰函数:

  1. 函数体除了typedef和静态元素,只允许有return语句
  2. 函数参数和返回值必须是字面值类型

在修饰变量时,包含有const的功能,二者区别不大,个人倾向于继续用const,觉得没必要去纠结太细微的区别。
constexpr 拓宽了常量表达式的范围,类似模板元编程,其定义的常量表达式,甚至是函数,可直接用于需要常量的运算,如:

constexpr int getSize1(int nNum)
{
	return nNum * nNum;
}

const int getSize2(int nNum)
{
	return nNum * nNum;
}

int main()
{
	const int nNum = 2;
	int arr1[getSize1(nNum)] = { 0 };	//< 编译通过
	int arr2[getSize2(nNum)] = { 0 };	//< 错误
	system("pause");
	return 0;
}

//! 输出
1>E:\01.study\C++\C++11\code\TestCpp11\TestCpp11\TestClass.cpp(218,20): error C2131: 表达式的计算结果不是常数

观点:这个特性在算法、嵌入式、底层基础库等注重效率的地方,存在一定的好处;但在更注重业务逻辑的业务层,如界面,上层业务,基本没用。

3.顶层/底层const

顶层const: 可以修饰任意的对象。定义指针时,表示指针本身是个常量
底层const: 只能修饰指针、引用等复合类型。定义指针时,表示指针所指的对象是个常量
注意:

  1. 当执行对象的拷贝操作时,顶层const不受什么影响,拷入和拷出的对象是否是常量没什么影响。底层const的限制不能忽视,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。
  2. 顶层const修饰函数参数时,经常会被忽略。如const std::string &,传入的类型可以是const类型、也可以是非const类型。这在函数转发(参数从a函数透传给b函数)时,需要特殊注意,参考第三章完美转发。

处理类型

1. using别名

C++11 引入了新的类型别名方式,用处不大,只是更“现代化”

using VCT_INT = std::vector<int>;

其等价于

typedef std::vector<int> VCT_INT;

using在派生类类中可以重新指定基类成员的访问权限,比如使基类的protect虚函数,在派生类中对用户可见。当using声明派生类的构造函数时,编译器会为其生成代码,继承基类的默认函数。详见:C++新标准,查漏补缺(3)类设计者的工具

2. auto类型

这真是个好东西,对C++的提升简直意义重大,省去不少类型定义的烦恼
如:

typedef std::vector<int> vctInt;
std::vector<int>::iterator it = vctInt.begin();

就可以简单的写成

typedef std::vector<int> vctInt;
auto it = vctInt.begin();

对于STL更复杂的定义,简直不要太方便
注意

  1. auto在定义同一行变量时,auto所指代的类型必须一致
auto i = 0, j = 1;			//< 正确
auto a = 1. b = 1.1; 	//< 错误,类型不一致
auto *x = &i, y = i;		//< 正确,auto等价于int,而定义出的x是int*,y是int类型
  1. auto的规则存在一定的不确定性
    如:auto i = 1;,编译器会判定i为int类型,而不是long,也不是int64,其原因是C++内部的隐式类型转换。而由于C++支持强制类型转换,理论上将上面的auto替换为其他类型都是能编译通过。
    C++的语言特点是强逻辑下的强规则,和auto这个特性有很大冲突,滥用可能导致语义不明,可读性下降。
    建议:
    (1)stl类型,可以用auto定义
    (2)自定义类型,局部使用auto
    (3)内建类型,原则上不使用auto ——实在没什么必要

3. decltype

这是对auto的一个补充特性

decltype(f()) x = v;	//< x的类型取决于f()的返回值推导(发生在编译期),而不是v
auto y = v;				//< 使用auto,则y的类型取决于v

比较有用的场景,函数模板的size_type,常规代码

VCT_INT vctInt;
for (std::vector::size_t i = 0; i < vctInt.size(); ++i)
	...

使用decltype,可以优化为

VCT_INT vctInt;
for (decltype(vctInt.size()) i = 0; i < vctInt.size(); ++i)
	...

不需要关注 vctInt.size() 的类型,避免警告发生,减少了代码信息量,扩展性还有所提升。

字符串,向量和数组

1. vector迭代器失效问题

vector的值会动态增长(其他很多stl容器也一样),在增、删vector元素时,均可能导致相关迭代器失效,如:

for (auto it = vct.begin(); it != vct.end(); ++it)
{
	if (*it == 1)
	{
		vct.erase(it);	//< 导致后续的++操作异常
	}
}

2. 指针也是迭代器

C++11中对指针增加了 begin(), end() 函数,用于确定常量指针的起始、结束位置。
98的感觉,迭代器的方法是仿造指针实现的
现在C++11,丰富了指针的方法,迭代器有的方法,普通指针都有了,指针反过来成为了迭代器的一种
这应该是 for (auto i : arr) 得以实现的基础
参考代码:

    int p[100] = { 0 };
    int* pBegin = std::begin(p);
    int* pEnd = std::end(p);
    printf("%lld == %lld\r\n", pEnd - pBegin, sizeof(p) / sizeof(int));	//< 输出100==100

表达式

1. 整数除法运算,对商取整的定义

早期版本:允许结果为负数的商向上或向下取整
C++11新标准规定:商一律向0取整,即直接切除小数部分

2. 相等性测试

对bool 类型的比较需要注意,一般会作为代码规范进行规定
注意:以下两个表达式的区别,特例 nValue=2

    //! 假定nValue是int类型
    //! 以下运算,将nValue转换为 bool 类型
    //! nValue == 0,为false,nValue != 0,为true
    if (nValue)
    {
        printf("ok");
    }
    
    //! 以下代码,会将 true 转换为 nValue 的类型进行比较
    //! 即 nValue == 1,为true,nValue != 1,为false
    if (nValue == true)
    {
        printf("ok");
    }

3. 位运算会对字节数少的对象首先提升到int类型再运算

    int num = ~'q' << 6;
    char c = ~'q' << 6;	//< 会报警告,int类型转换为char类型
    printf("0x%X\r\n0x%X\r\n", num, c);
    //! 输出:
    //! 0xFFFFE380
    //! 0xFFFFFF80

语句

1. 范围for循环

C++新特性,语法定义

for (declaration : exporession)
	statement
exporession:定义序列对象,即包含 begin()/end(),以及对应迭代器
declaration:定义一个变量,其类型可以是序列成员,也可以是通过序列成员转换获得

常规用法

for (auto item : v)
	statement

其等价于

for (auto beg = v.begin(), end = v.end(); beg != end; ++beg)
	statement

注意

  1. 可以作为序列对象的类型有很多,如STL容器(包括序列容器、关联容器),以及支持相关函数操作的 QString、Json::Value、初始化列表{数据}、数组(支持std::beign, std::end)
  2. 使用时,需要关注的是其元素类型,如vector的元素类型是指针
  3. 避免在循环中增加或删除元素值,会导致循环用的迭代器失效而异常

函数

1. initializer_list

函数如果要使用可变形参,可以使用 “…”。
如果所有形参都是同一类型,可以使用stl类 std::initializer_list 作为参数,如

bool func(int nValue, std::initializer_list<std::string> ilParam)
{
    for (auto &item : ilParam)
    {
        printf("%s\r\n", item.c_str());
    }
    return true;
}

//! 调用
func(1, { "1", "2", "3" });

问题:为什么要专门定义这样的一个类,直接用std::vector 或者 std::list不行么?从功能角度,bool func(int nValue, std::vector<std::string> ilParam) 和上述函数等价。
解释:这是C++11、编译器的一个约定,如下代码

std::vector<std::string> vct = { "1", "2", "3" };

等价于

std::initializer_list<std::string> ilst({"1", "2", "3"});
std::vector<std::string> vct(ilst);

编译器对花括号初始化列表,会创建一个 initializer_list<T> 对象,赋值给vector时,会调用vector对应的构造函数,同样这个适用于智能指针对象的传入,相当于统一了stl对象的构造行为。

_CONSTEXPR20 vector(initializer_list<_Ty> _Ilist, const _Alloc& _Al = _Alloc())

参考:https://stackoverflow.com/questions/14414832/why-use-initializer-list-instead-of-vector-in-parameters

2. 值是如何返回的

返回一个值的方式和初始化一个变量或形参的方式完全一样。
函数返回值,会创建一个临时变量(执行拷贝动作),用于赋值调用,当调用结束,该变量也将被销毁。
函数返回引用,则该引用仅是它所引用对象的一个别名,也即不存在临时变量一说。

class CMyObject
{
public:
    CMyObject()
    {
        printf("[%p]%s, %d\r\n", this, __FUNCTION__, __LINE__);
    }
	CMyObject(const CMyObject& obj)
	{
		printf("[%p]%s, %d\r\n", this, __FUNCTION__, __LINE__);
	}
    ~CMyObject()
    {
        printf("[%p]%s, %d\r\n", this, __FUNCTION__, __LINE__);
    }
    void print() const
    {
        printf("[%p]%s, %d\r\n", this, __FUNCTION__, __LINE__);
    }
};

void funCall(const CMyObject& obj)
{
    obj.print();
}

CMyObject getObject()
{
    CMyObject obj;
    return obj;
}

//! 调用
funCall(getObject());

//! 输出
[000000BAAD8FFAB4]CMyObject::CMyObject, 22
[000000BAAD8FFCB4]CMyObject::CMyObject, 26
[000000BAAD8FFAB4]CMyObject::~CMyObject, 30
[000000BAAD8FFCB4]CMyObject::print, 34
[000000BAAD8FFCB4]CMyObject::~CMyObject, 30

//! 对象创建、销毁顺序
1. getObject. “CMyObject obj;”,创建 000000BAAD8FFAB4、
2. getObject 退出后,执行拷贝,创建临时变量 000000BAAD8FFCB4
3. getObject 退出后,拷贝结束后,销毁函数内变量 000000BAAD8FFAB4
4. funCall 使用引用,此时未进行拷贝,直接调用变量 000000BAAD8FFCB4
5. funCall 执行结束后,销毁临时变量 000000BAAD8FFCB4

所以,特别注意:不要返回局部对象的引用或指针,因为当函数结束时,该变量已经被销毁,即对应的引用和指针都将失效,而导致异常。

3. 内联函数

定义:定义在类内部的成员函数是自动inline的;其余在外部定义,使用inline修饰,足够小且简单,由编译器认可的为内联函数。
作用:在每个调用节点内联展开进行调用
优点:减少函数调用出栈入栈的开销,提升函数调用效率
缺点:增加编译代码大小
如以下函数的调用

inline const string& shorterString(const string& s1, const string& s2)
{
	printf("shorterString\r\n");
	return (s1.length() > s2.length()) ? s1 : s2;
}
int main()
{
	string s1("12"), s2("234");
	cout << shorterString(s1, s1) << endl;
	system("pause");
}

main中调用内联函数,等价于

cout << (printf("shorterString\r\n"), (s1.length() > s2.length()) ? s2 : s1) << endl;

:内联函数在windows/VS编译器下,应用开发,使用较少,原因是VS的编译器对编译产物优化的很多,甚至没进行内联的,也给你搞成内联调用

4. 尾置返回类型

定义:形参列表后面跟“-> 返回类型”,形式auto func(param1, ...) -> retType {...}
作用:简化复杂返回类型的定义
举例,返回二维数组的函数:
常规函数声明

int (*funcRet(int i))[10]
{
	auto a = new int[i][10];
	return a;
}

采用尾置返回类型

auto funcRet2(int i) -> int(*)[10]
{
	auto a = new int[i][10];
	return a;
}

1. 默认构造函数

默认构造函数,即类控制执行默认的初始化过程的函数,该函数没有任何参数。
当且仅当类没有显示声明任何构造函数时,编译器为该类隐式定义默认构造函数,称为合成的默认构造函数。
举例

class CA {};
CA obj;

class CB
{
pubic:
    CB() = default;
    CB(int param) {}
};
CB objB;

以上,类CA,编译器会创建默认构造函数,可以直接定义obj。
类CB,虽然类已经定义了其他构造函数,但使用了= default,编译器会继续自动生成默认构造函数,default在内部,则生成内联默认构造函数;在类外部,则该函数就不是内类的。
对比不带默认构造函数的方式:

class CC
{
public:
    CC(int param){}
};
CC obj(123);

以上,类CC,定义了带参数的构造函数,也没用= default要求编译器自动生成默认构造函数,声明类对象时,必须调用对应构造函数,带上具体的参数。
:正常情况,我们并不期望出现默认构造函数,因为这会使成员函数初始化成不可知的值,导致未定义行为。

2. 可变数据成员

关键字:mutable
定义:该成员变量永远不会是const,就算包含它的类对象是const,或使用它的函数是const,该成员仍能被改变。
举例,以下代码是合法的:

#include <stdio.h>
#include <stdlib.h>
#include <string>

class CObject
{
public:
	void setString(const std::string& str) const
	{
		//! 在const 成员函数里,修改成员变量
		m_strData = str;
		printf("%s\r\n", m_strData.c_str());
	}

private:
	mutable std::string m_strData;
};

int main()
{
	CObject obj;
	obj.setString("abc");		//< 打印"abc"

	const CObject cstObj;		//< const对象中,修改成员
	cstObj.setString("123");	//< 打印"123"
	system("pause");
	return 0;
}

去除mutable,如下代码报错

#include <stdio.h>
#include <stdlib.h>
#include <string>

class CObject
{
public:
	void setString(const std::string& str)
	{
		//! 在const 成员函数里,修改成员变量
		m_strData = str;
		printf("%s\r\n", m_strData.c_str());
	}

private:
	std::string m_strData;
};

int main()
{
	const CObject cstObj;		//< const对象中,修改成员
	cstObj.setString("123");	//< 打印"123"
	system("pause");
	return 0;
}

报错内容

已启动生成...
1>------ 已启动生成: 项目: TestCpp11, 配置: Debug x64 ------
1>TestClass.cpp
1>E:\01.study\C++\C++11\code\TestCpp11\TestCpp11\TestClass.cpp(51,24): error C2662: “void CObject::setString(const std::string &)”: 不能将“this”指针从“const CObject”转换为“CObject &”
1>E:\01.study\C++\C++11\code\TestCpp11\TestCpp11\TestClass.cpp(51,2): message : 转换丢失限定符
1>E:\01.study\C++\C++11\code\TestCpp11\TestCpp11\TestClass.cpp(37,7): message : 参见“CObject::setString”的声明
1>已完成生成项目“TestCpp11.vcxproj”的操作 - 失败。
========== “生成”: 0 成功,1 失败,0 更新,0 已跳过 ==========

:这个关键字应用场景太少,使用这个关键字的场景,是否是设计不合理导致的?

3. 成员变量初始化

类的数据成员初始化有3种,初始化顺序 3 --> 2 --> 1

  1. 构造函数中初始化
  2. 构造函数使用初始化列表
  3. 新特性,类内初始化,即在声明成员时,采用=或{}列表初始化形式进行初始化

第3种初始化方案,这个特性在C++11到C++20变化较大,特殊考虑静态成员、非静态成员和兼容C++98 静态整形常量成员初始化语法,同时针对静态常量成员,增加了内联数据成员(用const static inline 修饰)
以下代码比较绕:

class CMyClass
{
	double m_a = 1.0;				//< 成功
	double m_b = 2.0;				//< 成功
	static double m_c = 3.0;		//< 失败,不允许初始化静态成员变量
	const double m_d = 4.0;			//< 成功
	const static double m_e = 5.0;	//< 失败,不允许初始化静态成员变量
	static int m_f = 6;				//< 失败,不允许初始化静态成员变量
	const static int m_g = 7;		//< 成功,兼容C++98标准,允许初始化静态int成员变量
	static inline double m_h = 8.0;	//< 成功
	const static inline double m_i = 9.0;	//< 成功
};

报错信息如下

已启动生成...
1>------ 已启动生成: 项目: TestCpp11, 配置: Debug x64 ------
1>TestClass.cpp
1>E:\01.study\C++\C++11\code\TestCpp11\TestCpp11\TestClass.cpp(64,25): error C2864: CMyClass::m_c: 带有类内初始化表达式的静态 数据成员 必须具有不可变的常量整型类型,或必须被指定为“内联”
1>E:\01.study\C++\C++11\code\TestCpp11\TestCpp11\TestClass.cpp(64,25): message : 类型是“double”
1>E:\01.study\C++\C++11\code\TestCpp11\TestCpp11\TestClass.cpp(66,31): error C2864: CMyClass::m_e: 带有类内初始化表达式的静态 数据成员 必须具有不可变的常量整型类型,或必须被指定为“内联”
1>E:\01.study\C++\C++11\code\TestCpp11\TestCpp11\TestClass.cpp(66,31): message : 类型是“const double”
1>E:\01.study\C++\C++11\code\TestCpp11\TestCpp11\TestClass.cpp(67,20): error C2864: CMyClass::m_f: 带有类内初始化表达式的静态 数据成员 必须具有不可变的常量整型类型,或必须被指定为“内联”
1>E:\01.study\C++\C++11\code\TestCpp11\TestCpp11\TestClass.cpp(67,20): message : 类型是“int”
1>已完成生成项目“TestCpp11.vcxproj”的操作 - 失败。
========== “生成”: 0 成功,1 失败,0 更新,0 已跳过 ==========

这里记录一个特殊的情况,后续在仔细查下

#include <stdio.h>
#include <stdlib.h>
#include <string>

std::string getString(int nNum)
{
	printf("%s:%d, %s, %d\r\n", __FILE__, __LINE__, __FUNCTION__, nNum);
	char szNum[1024] = { 0 };
	sprintf_s(szNum, sizeof(szNum), "%d", nNum);
	return szNum;
}

class CMyClass
{
public:
	CMyClass()
		: m_strB(getString(321))
	{
		printf("%s:%d, %s\r\n", __FILE__, __LINE__, __FUNCTION__);
	}

private:
	std::string m_strB{ getString(123) };
};

int main()
{
	CMyClass obj;
	system("pause");
	return 0;
}

注意:以上代码,在VS2022 debug C++17下,getString只执行了一次,输出如下,后续需要再进一步深入其原理。网上查到的信息(未考证),描述编译器最终会在构造函数初始化列表中实现内部初始化,即重复时,构造函数初始化列表覆盖内部初始化代码。

E:\01.study\C++\C++11\code\TestCpp11\TestCpp11\TestClass.cpp:106, getString, 321
E:\01.study\C++\C++11\code\TestCpp11\TestCpp11\TestClass.cpp:118, CMyClass::CMyClass
请按任意键继续. . .

4. 友元

一直以来我都是排斥友元的,我认为它破坏了类的封装性,让完好自洽的逻辑变得混乱。但还是有极特殊的场景会使用到,比如Qt中 QxxxPrivateData 的设计,将数据和业务分成两个对象进行管理,使用友元使类之间实现有限访问,不过我还是觉得这种设计是一种偷懒行为,并不是成熟代码。
这里还是对友元的语法进行记录,方便后续查阅。冷门的语法,不用就容易忘。
类友元函数

class CScreen
{
	//! 使能访问本类的私有成员
	friend void PrintSrceen(CScreen& obj);

private:
	void print()
	{
		printf("%s, %dx%d\r\n", __FUNCTION__, m_nWidth, m_nHeight);
	}

	int m_nWidth = 10;
	int m_nHeight = 10;
};

void PrintSrceen(CScreen &obj)
{
	obj.print();
	printf("%s, %dx%d\r\n", __FUNCTION__, obj.m_nWidth, obj.m_nHeight);
}
输出:
CScreen::print, 10x10
PrintSrceen, 10x10

类友元类

class CScreen
{
	//! 使能访问本类的私有成员
	friend class CPrinter;

private:
	void print()
	{
		printf("%s, %dx%d\r\n", __FUNCTION__, m_nWidth, m_nHeight);
	}

	int m_nWidth = 10;
	int m_nHeight = 10;
};

class CPrinter
{
public:
	void PrintSrceen(CScreen& obj)
	{
		obj.print();
		printf("%s, %dx%d\r\n", __FUNCTION__, obj.m_nWidth, obj.m_nHeight);
	}
};

int main()
{
	CScreen obj;
	CPrinter objPrinter;
	objPrinter.PrintSrceen(obj);
	system("pause");
	return 0;
}
输出:
CScreen::print, 10x10
CPrinter::PrintSrceen, 10x10

类友元类的成员函数
这里使友元的访问范围更严格,同时声明起来有点绕

//! 1. 提前声明,确保CPrinter::PrintSrceen声明通过
class CScreen;
//! 2. 声明必须在CScreen声明友元之前,否则友元的声明非法
class CPrinter
{
public:
	void PrintSrceen(CScreen& obj);
};

class CScreen
{
	//! 3. 使CPrinter中特定的函数,访问本类的私有成员
	friend void CPrinter::PrintSrceen(CScreen& obj);

private:
	void print()
	{
		printf("%s, %dx%d\r\n", __FUNCTION__, m_nWidth, m_nHeight);
	}

	int m_nWidth = 10;
	int m_nHeight = 10;
};

//! 4. 实现必须在CScreen声明友元之后,否则函数的定义非法
void CPrinter::PrintSrceen(CScreen& obj)
{
	obj.print();
	printf("%s, %dx%d\r\n", __FUNCTION__, obj.m_nWidth, obj.m_nHeight);
}

5. 操作符的重载

之前把操作符重载想复杂了,看到这么多参数,没有什么一致性就头疼,现在想想其实还是简单的,还是在通用规则的框架下。
首先,操作符重载有以下限制

1、不能重载的操作符
    域限定符 ::
    直接成员访问操作符 .
    三目运算符 ?:
    字节长度操作符 sizeof
   类型信息操作符 typeid
2、重载操作符不能修改操作符的优先级
3、无法重载所有基本类型的操作符运算
4、不能修改操作符的参数个数操作数
5、不能发明新的操作符
6. 只能作为成员函数重载:= 赋值运算符, []下标运算符, ()函数调用运算符, ->成员访问运算符,且是非静态成员,不能友元

其次,操作符重载,当成普通函数即可,仅限制了操作符的参数个数、返回类型,具体参数类型无所谓,设置返回类型也可再一定程度上忽略。
比如:

bool operator==(const std::string &left, const std::string &right);

写成

int operator==(const std::string &left, const char *szRight);

也是可以的,只是==操作符要求,要两个元素比较,同时返回比较结果,我这返回的是int,也是合理。
再次,操作符重载分全局、成员,全局就需要写出所有的操作对象,而成员操作符重载,已经提前放入了一个参数,就是当前对象。
以上"=="操作符可改写为

int operator==(const char *szRight);

最后,补充,没必要的话,我们就不要重载一些奇怪的操作符,如&& || &,因为在C++中已经定义了其对类对象的操作, 重载该运算符会导致丧失一部分功能, 为类的使用者带来麻烦。正常的重载,理论上不会有什么问题。

6. 默认赋值操作

C++默认赋值操作,即operator=,如A=B,会将B的成员(非static)逐一赋值给A的成员,调用=号运算符。
但是,仅仅是赋值,针对指针类型,也只是赋值指针的值,而不对指针进行拷贝。
所以:我们一般不适用默认赋值操作,需要显示定义operator=操作符,不过我们一般再定义个自定义函数名,使其意义更明确,比如A.copy(B)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

求知向道

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值