C++新标准,查漏补缺(4)高级主题

6 篇文章 0 订阅

目录

标准库的特殊设施

1. std::tuple

2. 随机数

3. 正则表达式

3.1 概述

3.2 构建regex

3.3 smatch匹配结果

3.4 ssub_match子表达式结果

3.5 regex_match/regex_search 单次查找

3.6 sregex_iterator遍历匹配

3.7 使用子表达式进行数据格拆分

3.8 regex_replace遍历替换

4. IO库

4.1 格式化操纵符

4.2 未格式化输入/输出

4.3 流随机访问

用于大型程序的工具

1. 异常处理

1.1 抛出异常

1.2 捕获异常

 1.3 noexcept声明

1.4 异常类层次

2. 命名空间

2.1 命名空间分类

2.2 类、命名空间与作用域

2.3 命名空间与重载

3. 多重继承 

3.1 构造函数

3.2 多重继承下的类作用域

3.3 虚继承

特殊工具与技术

1. 控制内存分配

1.1 重载new和delete

1.2 定位new表达式

2. 运行时类型标识

3. 枚举类型

4. 类成员指针

4.1 成员数据指针

4.2 成员函数指针

4.3 将成员函数作为可调用对象

5. union

6. 位域

7. volatile限定符

8. 链接指示extern "C"


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

标准库的特殊设施

1. std::tuple

作用

  1. 当我们期望将一些数据组合成单一对象,但又不想麻烦地定义一个心数据结构来表示这些数据
  2. 可以让函数返回多个值(配合auto可以规避变量复杂声明的问题),同python等现代化语言对齐,python函数天生就支持多种形式的返回

构建

std::tuple<T1, T2, ..., Tn> t;    //< 创建默认初始值的tuple
std::tuple<T1, T2, ..., Tn> t(v1, v2, ..., vn);    //< 创建指定初始值的tuple
std::make_tuple(v1, v2, ..., vn); //< tuple便利函数,类似make_pair,返回指定初始值的tuple

所有tuple的构造都是explicit的,必须使用直接初始化语法

比较

t1 relop t2 (relop指关系操作符,>, <, >=, <=, ==,!=)

要求t1和t2类型必须一致,其中运算主要依赖:<、== 操作

取值

get<i>(t)

i必须是整形常量表达式

运算

tuple_size<tupleType>::value //< 返回tupleType中的成员数量
tuple_element<i, tupleType>::typle //< 返回tupleType中第i个(从0开始)成员类型

2. 随机数

作用

旧版C++在随机数上比较简单,通过seed()、rand()来实现,该方案只能均匀分布,如果要特定范围、特殊概率分布,需要增加逻辑代码。新版本对该内容进行扩展。

概念

概念作用对象举例
随机数引擎生产unsigned int随机数序列,可直接替代seed()、rand()

std::default_random_engine:高效产生伪随机数(需要指定随机数种子)

std::random_device:基于电脑硬件的真随机数

一般将 random_device 生成一个随机数,作为种子传入 default_random_engine 来生产随机数

随机数分布基于随机数引擎,生成指定类型的,在给定范围内,服从特定概率分布的随机数

std::uniform_int_distribution<type> u(0, 9)

生成0~9的均匀分布的数,默认type=unsigned int

std::bernoulli_distribution u((double)30/100);

生成随机的false/true,其中产生true概率占30%

std::uniform_real_distribution<type> d(0, 1);

生成0~1随机分布的浮点数,默认type=double

std::normal_distribution<type> n(4, 1.5);

生成均值为4,标准差为1.5的随机数,默认type=double

随机引擎操作函数

Engine e

Engin e(s)

创建引擎对象,s为随机数种子,一般为unsigned int
e.seed(s)设置随机数种子s,一般为unsigned int
e()即调用e.operator(),生成一个随机数

e.min()

e.max()

该引擎所能生成的随机数最大和最小值
e.discard(u)将随机数引擎推进u步,u为uint64
Engine::result_type引擎生成的数据类型

default_random_engine伪随机数示例

template <typename EngineT>
void tesetRandomDevice(std::vector<int> &vctOut)
{
	EngineT e;
	for (int i = 0; i < 10; ++i)
	{
		vctOut.push_back(e());
	}
}

{
	std::vector<int> vctOut1, vctOut2;
	tesetRandomDevice<std::default_random_engine>(vctOut1);
	tesetRandomDevice<std::default_random_engine>(vctOut2);
    //! 输出为true
	std::cout << std::boolalpha << (vctOut1 == vctOut2) << std::noboolalpha << std::endl;
	std::cout << "first" << std::endl;
	for (auto it : vctOut1)
	{
		std::cout << it << std::endl;
	}
	std::cout << "second" << std::endl;
	for (auto it : vctOut2)
	{
		std::cout << it << std::endl;
	}
}

random_device真随机数,同样代码,修改类型

{
	std::vector<int> vctOut1, vctOut2;
	tesetRandomDevice<std::random_device>(vctOut1);
	tesetRandomDevice<std::random_device>(vctOut2);
    //! 输出为false
	std::cout << std::boolalpha << (vctOut1 == vctOut2) << std::noboolalpha << std::endl;
	std::cout << "first" << std::endl;
	for (auto it : vctOut1)
	{
		std::cout << it << std::endl;
	}
	std::cout << "second" << std::endl;
	for (auto it : vctOut2)
	{
		std::cout << it << std::endl;
	}
}

随机数分布操作函数

Dist<Type> d(init)

Dist<> d(init)

std::bernoulli_distribution d((double)30/100);

构建随机数分布对象

除了bernoulli_distribution,其他随机数分布都是模板类,且Type都有默认参数

init,依据不同的类型,传入不同的初始值

d(e)通过随机数引擎e,生成一个随机数

d.min()

d.max()

所能生成的最大值、最小值
d.reset()重置,下一个生成的值,不再依赖已生成的值

随机分布案例

	//! 构建引擎
    std::random_device e2;
    std::default_random_engine e;
    e.seed(e2());

    //! 0-9分布
    {
		std::uniform_int_distribution<unsigned> u(0, 9);
		std::vector<int> vctCount(10, 0);
		const int CST_RUN_TIME = 100000000;
		for (auto i = 0; i < CST_RUN_TIME; ++i)
		{
			auto elem = u(e);
			++vctCount[elem];
		}

        //! 概率计算
		for (auto it : vctCount)
		{
			double dCalc = (double)it / CST_RUN_TIME;
			std::cout << it << ": " << dCalc << std::endl;
		}
    }

0~9触发次数,以及输出概率:

9994219: 0.0999422
9999112: 0.0999911
10004428: 0.100044
9998716: 0.0999872
9997484: 0.0999748
10001689: 0.100017
9999064: 0.0999906
10001230: 0.100012
9999863: 0.0999986
10004195: 0.100042

3. 正则表达式

3.1 概述

C++11新引入了正则表达式功能,在头文<regex>中定义相关功能,主要功能有:文本验证、查找、切割和替换。

使用规范:应避免滥用正则,避免使用复杂正则,如需正则,应该进行注释说明,并对相关表达式进行严格测试(最好是单元测试,测试各类异常、边界条件)

相关概念

1. 正则类,regex/wregex,构建正则对象

2. 正则函数,regex_xxx,使用正则对象,进行文本操作,并保存结果在match结果中

3. 正则结果,xmatch(如smatch),正则搜索结果,xsub_match(如ssub_match),子表达式结果

4. 迭代器,xregex_iterator(如sregex_iterator),用于遍历regex搜索结果

以上对象,针对不同类型的字符串string、wstring、char、wchar_t,正则类、正则结果、迭代器需要使用不同对象,且要互相匹配,否则编译不通过。相关列表如下,后续案例以string说明

字符串类型正则类匹配结果子表示匹配结果匹配结果迭代器
string

regex

smatchssub_matchsregex_itrerator
const char *regexcmatchcsub_matchcregex_itrerator
wstringwregexwsmatchwssub_matchwsregex_itrerator
const wchar_t *wregexwcmatchwcsub_matchwcregex_itrerator

regex定义如下,基于basic_regex

_EXPORT_STD using regex   = basic_regex<char>;
_EXPORT_STD using wregex  = basic_regex<wchar_t>;

match定义如下,基于match_results

_EXPORT_STD using cmatch  = match_results<const char*>;
_EXPORT_STD using wcmatch = match_results<const wchar_t*>;
_EXPORT_STD using smatch  = match_results<string::const_iterator>;
_EXPORT_STD using wsmatch = match_results<wstring::const_iterator>;

regex_iterator定义如下,基于“regex_iterator”

_EXPORT_STD using cregex_iterator  = regex_iterator<const char*>;
_EXPORT_STD using wcregex_iterator = regex_iterator<const wchar_t*>;
_EXPORT_STD using sregex_iterator  = regex_iterator<string::const_iterator>;
_EXPORT_STD using wsregex_iterator = regex_iterator<wstring::const_iterator>;

常规使用流程,以string为例:

1. 构建regex类

2. 使用regex_xxx,执行正则流程

3. 使用smatch,获取正则结果

3.2 构建regex

regex主要函数

函数功能

regex r(re)

regex r(re, f)

构造函数

re,正则表达式字符串,依据regex类型传入const char *,std::string

f,regex_constants::syntax_option_type类型,指定正则标准,默认使用ECMAScript

r=re

r.assign(re, f)

重新设置正则表达式,参数同构造函数
r.mark_count()r中子表达式的树木
r.flags()同构造中的标志

regex_constants::syntax_option_type标志,包括语法类型和行为标志,只能指定一个语法,零个或多个标志可以与语法组合,以指定正则表达式引擎行为。 如果仅指定标志,则假定语法为 ECMAScript。

语法类型如下:

标志说明
ECMAScript = 0x01基于ECMAScript标准的语法。JavaScript、ActionScript和Jscript等语言都使用该标准
basic  = 0x02基本的POSIX语法
extended = 0x04拓展的POSIX语法
awk = 0x08POSIX awk实用工具使用的语法
grep = 0x10POSIX grep实用工具使用的语法
egrep = 0x20用逗号分隔的POSIX egrep语法

标志类型如下:

标志说明
icase    = 0x0100忽略大小写
nosubs   = 0x0200不保存匹配的子表达式
optimize = 0x0400加快匹配速度,代价是构造时间可能更长
collate  = 0x0800使用区分区域设置的排序规则序列(例如 [a-z] 形式的范围)

错误类型

一个正则表达式的语法是否正确是在运行时解析的,当正则表达式构建错误时,会抛出regex_error错误

try
{
	std::string strSrc("123456abcdefg");
	std::regex reg("\\d+[", std::regex_constants::ECMAScript);
	std::smatch mr;
	bool bRes = regex_match(strSrc, mr, reg);
	std::cout << std::boolalpha << bRes << std::noboolalpha
		<< " match " << mr.str()
		<< std::endl;
}
catch (const std::regex_error &e)
{
	std::cout << "regex_error: " << e.what() << "\r\n"
		<< "code: " << e.code()
		<< std::endl;
}
catch (const std::exception &e)
{
	std::cout << "exception: " << e.what() << "\r\n"
		<< std::endl;
}

//! 输出
//! regex_error: regex_error(error_brack): The expression contained mismatched [ and ].
//! code: 4

使用规范:构建一个正则表达式及其费时间,应避免创建不必要的正则表达式

3.3 smatch匹配结果

smatch用于存储匹配结果,主要函数

函数功能
m.ready()返回true,表示已经通过regex_search或regex_match设置m进行了操作,可以使用m进一步操作。
m.size()

0,匹配失败

>0,匹配成功,返回正则表达式中子表达式的数量

m.empty()size()为0时,返回true
m.prefix()返回一个ssubmatch对象,表示匹配前的序列
m.subfix()

返回一个ssubmatch对象,表示匹配后的序列

m.format(dest, fmt, mft)

m.format(fmt, mft)

格式化输出,使用格式字符串fmt生成格式化输出

fmt:格式化标识符,使用“$数字”(如 $1),表示取值第n(从1开始)个子序列的字符串,$0表示全部字符串。

dest:格式化的内容,输出到额外的字符串的起始位置,可以是迭代器,也可以是指针地址

mft:regex_constants::match_flag_type类型,指定格式化方式,默认match_default

返回:函数1返回dest的尾置迭代器,或指针结尾(书上没写,实际测试的结果,存疑)。函数2返回格式化后的字符串。

m.length(n)第n个匹配的子表达式的大小
m.position(n)第n个子表达式距序列开始位置的距离
m.str(n)第n个子表达式匹配的string,n默认为0,即全部字符串
m[n]对应第n个子表达式的ssub_match对象

m.begin()

 m.end()

m.cbegin()

m.cend()

表示m中ssub_match元素范围的迭代器,与往常一样,cbegin、cend返回const_iterator

regex_constants::match_flag_type,该枚举包括匹配标志和格式化标志,可使用|组合使用

使用到格式化标志如下

标志说明
format_default使用 ECMAScript 格式规则,默认参数,具体格式不明
format_sed使用 sed 格式规则,具体格式不明
format_no_copy不复制与正则表达式不匹配的文本
format_first_only不搜索第一个匹配项之后的匹配项

3.4 ssub_match子表达式结果

正则中通用有一个或多个子表达式,子表达式是模式的一部分,本身也有意义,通常用()来区分。在匹配过程,生成smatch,期间包含各个子表达式的匹配结果,即ssub_match。

子表达式匹配操作的返回结果,主要函数

函数功能
matched一个public bool数据成员,支出此sub_match是否匹配

first

second

public数据成员吗,指向匹配序列首元素和尾后元素的迭代器,如果未匹配,则first==second
length()匹配的大小,如果matched为false,返回0
str()返回一个包含输入中匹配部分的string,如果matched为false,返回空string
s=ssub

将ssub_match对象ssub装换为string对象s,等价于“s=ssub.str()”。转换运算符不是explicit的。

子表达式可以用于

1. 正则匹配进行粗筛选,再用子匹配进行深层次校验

2. 用于匹配后的文本重新格式化

3.5 regex_match/regex_search 单次查找

regex_match 要求字符串与正则表达式完全匹配,返回true,否则返回false

regex_search 寻找第一个匹配的序列,成功返回true,否则返回false

以上函数有统一的函数格式,说明如下

函数参数说明

(seq, m, r, mft)

(seq, r, mtf)

seq,待匹配的字符串序列

m,匹配结果保存,match对象

r,regex对象

mft,匹配参数regex_constants::match_flag_type

返回:匹配成功true,失败false

regex_constants::match_flag_type,该枚举包括匹配标志和格式化标志,可使用“|”组合使用

使用到匹配标志如下,一般match_default就可以了

标志说明
match_default = 0x0默认标志,无动作
match_not_bol = 0x01不将目标序列中的第一个位置视为行首
match_not_eol = 0x02不将目标序列中超过末尾的位置视为行尾
match_not_bow = 0x04不将目标序列中的第一个位置视为单词的开头
match_not_eow = 0x08不将目标序列中超过末尾的位置视为单词的结尾
match_any = 0x10如果有多个匹配,任何匹配都是可接受的
match_not_null = 0x20不要将空的子序列视为匹配项
match_continuous = 0x40不搜索除目标序列开头位置之外的匹配项
match_prev_avail = 0x100是有效的迭代器,请忽略 --firstmatch_not_bol 和 match_not_bow(如果已设置)

使用案例

//! 查找src中的数字
std::string strSrc("123456abcdefgAAA");
std::regex reg("\\d+", 
	std::regex_constants::ECMAScript | std::regex_constants::icase);
std::smatch mr;

bool bRes = regex_match(strSrc, mr, reg);
//! 输出,false match 
std::cout << std::boolalpha << bRes << std::noboolalpha
	<< " match " << mr.str()
	<< std::endl;

//! 输出:true search 123456
bRes = regex_search(strSrc, mr, reg);
std::cout << std::boolalpha << bRes << std::noboolalpha
	<< " search " << mr.str()
	<< std::endl;

3.6 sregex_iterator遍历匹配

使用迭代器sregex_iterator,进行循环操作,该迭代器将调用regex_search函数,并返回smatch对象,实现正则匹配的遍历

sregex_iterator主要函数

函数功能
sregex_iterator it(b, e, r)

b、e,string的起始、结束迭代器

r,regex对象

构造函数,将调用regex_search,并将it定位到输入中第一个匹配的位置

sregex_iterator itEnd;sregex_iterator的尾置迭代器,作为遍历结束判断条件

*it

it->

更具最后一个调用regex_search的结果,返回一个smatch对象的引用或指针

it++

++it

查找下一个匹配字符串

it1 == it2

it1 != it2

一般用于和尾置迭代器判断

 例子

std::string strSrc("123456abcdefgAAA");
std::regex reg("[a-z]", std::regex_constants::icase);
for (std::sregex_iterator it(strSrc.begin(), strSrc.end(), reg), itEnd;
	itEnd != it; ++it)
{
	std::cout << "match: " << it->str() << std::endl;
}

由于指定了icase,输出abcdefgAAA

3.7 使用子表达式进行数据格拆分

案例,使用正则表达式分析邮箱信息

std::vector<std::string> vctSrc = {
	"abc.com",
	"hoao@abc.cm",
	"hello@qq.net"};
std::regex reg("^([a-zA-Z0-9_%+-]+)@([a-zA-Z0-9-]+)\.([a-zA-Z]{2,})$");
for (auto itSrc : vctSrc)
{
	for (std::sregex_iterator itMatch(itSrc.begin(), itSrc.end(), reg), itEnd;
		itEnd != itMatch; ++itMatch)
	{
		std::cout << "name: " << (*itMatch)[1] << "\r\n"
			<< "domain: " << (*itMatch)[2] << "\r\n"
			<< "host: " << (*itMatch)[3] << "\r\n"
			<< "0: " << (*itMatch)[0] << std::endl;   //< 全部文本
	}
}

以上提取出符合要求的邮箱数据,并解析邮箱信息。我们可以进一步处理处理,包括用户名匹配,邮箱分类等。案例输出

name: hoao
domain: abc
host: cm
0: hoao@abc.cm
name: hello
domain: qq
host: net
0: hello@qq.net

3.8 regex_replace遍历替换

regex_replace函数说明

函数说明

(seq, r, fmt, mft)

seq,待查找序列,替换不会改变原有的字符串

r,regex对象

fmt,计划替代的文本格式,同match.format

mtf,regex_constants::match_flag_type类型,同时使用匹配标志和格式化标志

返回:替换后字符序列
 

函数实际调用 regex_replace(back_inserter(result), str.begin(), str.end(), re, fmt, flags)。 它将返回 result

(out, begin, end, r, fmt, mft)

out,输出替换后的文本位置,指向文本末尾

begin,待查找文本序列,起始迭代器

end,待查找文本序列,尾置迭代器

r,fmt,mft,同上
 

函数功能:对[begin, end)逐一匹配,对每个子序列,使用format函数替换为文本fmt


详细逻辑(参考msdn):
1. 构建regex_iterator(begin,end,r,mft),对待查文本进行匹配,生成子序列t0-tn,匹配match结果m0-mn,未匹配到内容,0标识整个输入序列,且N为0
2. 如果指定format_first_only,则仅使用第一个匹配项,T1 是匹配项后跟的全部输入文本,且 N 为 1。

3. 如果未指定format_no_copy,对于 [0, N) 范围内的每个 i,则将 Ti 范围内的文本复制到迭代器 out。 然后它调用 m.format(out, fmt, flags),其中 m 是迭代器对象 iter 为子序列 Mi 返回的 match_results 对象。

4. 如果未指定format_no_copy,将范围 TN 内的文本复制到迭代器 out。 该函数返回 out,out指向文本末尾。

案例

std::string strSrc("123456abcdefgAAA");
std::regex reg("(\\d)",     //< 使用()产生子序列匹配
	std::regex_constants::ECMAScript | std::regex_constants::icase);


//! 只对第一个匹配内容,即文本“1”进行替换
std::string strRes = regex_replace(strSrc, reg, std::string("[$1]"),
	std::regex_constants::format_first_only);
std::cout << "replace " << strSrc
	<< " res " << strRes    //< 输出 “[1]23456abcdefgAAA”
	<< std::endl;

//! 对全部子序列进行替换
strRes.clear();
auto it = regex_replace(std::back_inserter(strRes), strSrc.begin(), strSrc.end(),
	reg, std::string("($1)"));
std::cout << "replace2 " << strSrc
	<< "res " << strRes    //< 输出(1)(2)(3)(4)(5)(6)abcdefgAAA“”
	<< std::endl;

4. IO库

IO库iostream,正常应用使用的机会比较少,我还是喜欢printf的用法,简单清楚。但是调试打印、日志打印等小概率会碰到点,这里做个记录。

4.1 格式化操纵符

操纵符用于两大类输出控制:控制数值的输出形式,控制补白的数量和位置。

当操纵符改变流的格式状态时,大部分对后续所有IO都生效,所以大多数改变格式状态的操纵符都是设置/恢复成对的。

操纵符列表:

操纵符功能

boolalpha

true、false是否显示字符串

noboolalpha(默认)true、false显示0/1

showbase

noshowbase(默认)

是否显示进制前缀

showpoint

noshowpoint(默认)

当浮点数包含小数部分时,是否显示小数点

uppercase

nouppercase(默认)

在十六进制中打印A-F时、科学计数法打印E时,是否显示大写

dec(默认)

oct

hex

设置整型数值的进制(不包括浮点值),十进制/八进制/16进制
setfill

指定一个字符,代替默认的空格,作为补白输出

setw

仅下一个打印生效,不改变流的内部状态

设置输出或字符串值的最小空间

left左对齐,在右侧添加填充字段
right右对齐,在左侧添加填充字段
inernal在符号和值之间添加填充字段
fixed浮点值显示为定点十进制
scientific浮点值显示为科学记数法
hexfloat浮点值显示为十六进制(C++11)
defaultfloat重置浮点数格式为十进制(C++11)

unitbuf

nounitbuf(默认)

每次输出都刷新缓存区

恢复正常的缓冲区刷新方式

flush刷新ostream缓冲区
ends插入空字符,然后刷新ostream缓冲区
endl插入空行,然后刷新ostream缓冲区
skipws(默认)输入运算符,跳过空白字符
noskipws输入运算符,不跳过空白字符
setprecision(n)将浮点精度设置为n
setbase(b)将整数输出为b进制

案例,实现对齐补白

void testIO()
{
	double f = 10000.1234, f2 = 0.1;
	std::cout << std::fixed << std::setfill('*') << std::setprecision(2);
	std::cout << std::right 
		<< std::setw(10) << f << std::setw(10) << f2 << "\r\n"
		<< std::setw(10) << f2 << std::setw(10) << f << "\r\n"
		<< std::endl;

	std::cout << std::left
		<< std::setw(10) << f << std::setw(10) << f2 << "\r\n"
		<< std::setw(10) << f2 << std::setw(10) << f << "\r\n"
		<< std::endl;

	std::cout << std::internal
		<< std::setw(10) << f << std::setw(10) << f2 << "\r\n"
		<< std::setw(10) << f2 << std::setw(10) << f << "\r\n"
		<< std::endl;
}

输出

**10000.12******0.10
******0.10**10000.12

10000.12**0.10******
0.10******10000.12**

**10000.12******0.10
******0.10**10000.12

请按任意键继续. . .

4.2 未格式化输入/输出

单字节操作,同时会读取空白字符

操作功能
is.get(ch)读取ch
os.put(ch)输出ch
is.get()读取下一个字节作为int返回,并删除
is.putback(ch)ch放回is
is.unget()忽略一个字节
is.peek()读取下一个字节,作为int返回,但不删除它

多字节操作

操作功能
is.get(sink, size, delim)

读取最多size字节,存入sink

当遇到字符delim、EOF,或读取了size字节后终止,delim/换行符 不写入sink,但保留在流中,会被下次读取操作读取

is.getline(sink, size, delim)同get,delim不写入sink,并且同换行符一起在流中丢弃
is.read(sink, size)读取最多size个字节,存入sink中
is.gcount()返回上一个未格式化读取操作从is读取的字节数
os.write(source, size)将source中的size个字符,写入os
is.ignore(size, delim)读取忽略最多size个字符,包括dellim,size默认1,delim默认eof

流状态标志,保留cin流操作状态

状态说明
goodbit无错误
badbit无法恢复的错误
failbit

I/O 操作失败,例如格式设置或提取错误

假如get等获取到空串,将返回错误

eofbit流已到达文件末尾
状态函数说明
void clear(iostate state = goodbit, bool reraise = false);
void clear(io_state state);

功能:清除异常

state
(可选)清除所有标志后,设置你想要的标志。默认为 goodbit。
reraise
(可选)指定是否应重新引发这个异常。默认为 false(将不重新引发异常)。

bool good()

bool bad()

bool eof()

bool fail()

测试相关标志是否设置
iostate rdstate() const;读取状态标志
void setstate(iostate _State);设置状态

案例,测试get/getline/ignore/state

char szData[100] = { 0 };
std::cin.get(szData, 10, 'a');
std::cout << ">>>" << szData << "<<<" << std::endl;
std::cin.ignore(2);	//< 由于'a'、\n 残留在is中,跳过2个字符
std::cout << "State:" << std::cin.rdstate() << std::endl;

memset(szData, 0, sizeof(szData));
std::cin.get(szData, 10, 'a');
std::cout << ">>>" << szData << "<<<" << std::endl;
std::cin.ignore(2);	//< 由于'a'、\n 残留在is中,跳过2个字符
std::cout << "State:" << std::cin.rdstate() << std::endl;

//! 如果设置了failbit,流将对后面的输入或输出关闭,直到位被清除
//std::cin.setstate(std::ios::failbit);
memset(szData, 0, sizeof(szData));
std::cin.getline(szData, 10, 'a');
std::cout << ">>>" << szData << "<<<" << std::endl;
std::cout << "State:" << std::cin.rdstate() << std::endl;
	
memset(szData, 0, sizeof(szData));
std::cin.getline(szData, 10, 'a');
std::cout << ">>>" << szData << "<<<" << std::endl;
std::cout << "State:" << std::cin.rdstate() << std::endl;

4.3 流随机访问

主要用于文件流操作,访问数据的指定位置

函数说明

tellg()

tellp()

g,从流中获取

p,写入到流中

获取当前位置

seekg(pos)

seekp(pos)

重定位到给定的绝对位置

seekg(off, from)

seekp(off, from)

定位到from之前或之后off个字符

from可填:

beg,开始位置

curr,当前位置

end,结尾位置

案例

std::stringstream ss;
ss << "1234567890abcdefg";

//! 打印字符串
std::cout << ss.str() << std::endl;
//! 初始流位置
std::cout << "out: " << ss.tellg() << " in: " << ss.tellp() << std::endl;
		
//! 修改输入流起始位置
ss.seekp(5);
std::cout << "out: " << ss.tellg() << " in: " << ss.tellp() << std::endl;

ss << "uuu";	//< 从6开始覆盖
std::cout << ss.str() << std::endl;
std::cout << "out: " << ss.tellg() << " in: " << ss.tellp() << std::endl;

//! 修改输出流起始位置
ss.str("");	//< 清空流
ss << "1234567890abcdefg";
ss.seekg(10);
std::cout << "out: " << ss.tellg() << " in: " << ss.tellp() << std::endl;

std::string strPut("");
ss >> strPut;	//< 输出abacdefg,ss产生eof错误,再次使用需要clear
std::cout << ss.str() << std::endl;
std::cout << strPut << std::endl;
std::cout << "out: " << ss.tellg() << " in: " << ss.tellp() << std::endl;

//! 状态读取,清空后,再探
std::cout << ss.rdstate()
	<< " fail:" << ss.fail()
	<< " bad:" << ss.bad()
	<< " eof:" << ss.eof()
	<< std::endl;
ss.clear();
std::cout << ss.rdstate()
	<< " fail:" << ss.fail()
	<< " bad:" << ss.bad()
	<< " eof:" << ss.eof()
	<< std::endl;

//! 查看实际位置
std::cout << "out: " << ss.tellg() << " in: " << ss.tellp() << std::endl;
ss.seekg(0, std::ios::beg);
ss.seekp(0, std::ios::end);
std::cout << "out: " << ss.tellg() << " in: " << ss.tellp() << std::endl;

用于大型程序的工具

1. 异常处理

异常一直是我比较抗拒的代码风格,由于作用域的问题,变量定义、使用很麻烦;由于异常信息被封装的问题,不知道该catch哪些内容,以及害怕catch内容过少等问题,不如java能自动生成catch;由于中断的语法,可能导致内存泄漏、逻辑异常等问题。实在不知道这个有什么好的,所以一直不怎么会用。

C++11的标准库实在太多,异常处理的场景也多了起来,做个记录。

1.1 抛出异常

异常
即异常对象,是一种特殊对象,编译器使用异常抛出表达式来对异常进行拷贝初始化。

抛出

即throw,语法特性

  1. 沿着调用链的函数可能会提早退出
  2. 一旦程序开始执行异常处理代码,则沿着调用了创建的对象会被销毁(栈对象)

throw的语法规则类似于return。

栈展开

即匹配catch语句的过程。

如果没有找到匹配的catch,则退出当前这个主调函数,继续在调用了刚退出的这个函数的其他函数中寻找,依此类推。如果持续没有找到匹配的catch语句,程序退出调用terminate。

  1. 如果在栈展开过程中退出了某个块,编译器将负责确保这个块中创建的对象能被正确地销毁
  2. 如果某个局部对象的类型是类类型,则该对象的析构函数将被自动调用
  3. 如果异常发生在构造函数中,则当前的对象可能只构造了一部分。即使某个对象只构造了一部分,我们也要确保已构造的成员能被正确的销毁。
  4. 析构函数不应该抛出不能被他自身处理的异常。所有标准库类型都能确保它们的析构函数不会引发异常。

1.2 捕获异常

catch子语句的异常声明要求

  1. 声明的类型必须是完全类型
  2. 不能是右值引用,可以是左值引用
  3. catch的参数类型,是非引用类型,则该参数是异常对象的一个副本;是引用类型,该参数是异常对象的别名,即所做的修改,会影响后续的catch语句
  4. 如果catch的参数是类类型,则catch无法使用派生类的特有的任何成员,但可以使用派生类类型的异常对象对其进行初始化

catch查找匹配
在搜寻catch语句的过程中,我们最终找到的catch未必是异常的最佳匹配,而是第一个与异常匹配的catch语句。因此,越是专门的catch越应该置于整个catch列表的前端。

匹配规则

  1. 允许从非常量向常量类型的转换
  2. 允许从派生类向基类的类型转换
  3. 数组被转换成数组元素类型的指针
  4. 函数被转换陈指向该函数类型的指针

重新抛出
有时,一个单独的catch语句不能完整地处理某个异常,可以在catch分支中使用

throw;

由调用链更上一层函数接着处理异常

try...catch...案例

void testException()
{
	try
	{
		long long llSize = INTMAX_MAX;
		int* pData = new int[llSize];
		memset(pData, 0, sizeof(int) * llSize);
	}
	catch (std::exception& e)
	{
		std::cout << "ex: " << e.what() << std::endl;
	}
	catch (...)
	{
        //! 捕获所有异常
		std::cout << "default exception" << std::endl;
	}
}

//! 输出
ex: bad array new length
请按任意键继续. . .

构造函数初始化列表捕获异常

处理构造函数初始值抛出的异常,必须将构造函数写成函数try语句块(也称为函数测试块)的形式

template <typename T>
class CTestExcept
{
public:
	CTestExcept(std::initializer_list<T> il) 
        //! 特殊语法,try:,捕获初始列表异常
		try:
			m_data(std::make_shared<std::vector<T>>(il))
	{
        //! 函数主体
		long long llSize = INTMAX_MAX;
		int* pData = new int[llSize];
		memset(pData, 0, sizeof(int) * llSize);

		printf("%s,%d\r\n", __FILE__, __LINE__);
	}
	catch (const std::bad_alloc &e)
	{
		std::cout << "bad alloc: " << e.what() << std::endl;
	}
	catch (const std::exception &e)
	{
		std::cout << "exception: " << e.what() << std::endl;
	}
	catch (...)
	{
		std::cout << "default exception" << std::endl;
	}

	~CTestExcept()
	{
	}

	void print()
	{
		for (auto &it : *(m_data.get()))
		{
			std::cout << it << std::endl;
		}
	}

private:
	std::shared_ptr<std::vector<T>> m_data;
};

 1.3 noexcept声明

noexcept使用规则

  1. 在函数的尾置返回类型之前声明
  2. 必须同时出现在该函数的所有声明语句和定义语句中
  3. 可以在函数指针的声明和定义中指定
  4. 在typedef或类型别名中不能出现
  5. 在成员函数中,noexcept说明符需要在const及引用限定符,而在final、override或虚函数=0之前声明

违反异常说明
如果一个函数在说明了noexcept的同时,又含有throw语句或者调用了可能抛出异常的其他函数,程序就会调用terminate以确保遵守不在运行时抛出异常的承诺。
所以noexcept用在以下场景

  1. 我们确认函数不会抛出异常
  2. 我们不知道该如何处理异常

noexcept运算

异常说明实参

noexcept(true):不会抛出异常

noexcept(false):可能抛出异常

异常说明运算符

noexcept(e)

返回一个bool类型的右值常量表达式,当e调用的所有函数都做了不抛出说明,且e本身不含有trhow语句时,上述表达式为true;反之为false.

以上实参+运算符结合运用,一定程度简化noexcept的声明(虽然意义不大,因为存在不同结果,调用者就需要关注细节了,且可能间接函数并未声明noexcept,导致判断不准确)

template <typename T>
class CTestExcept
{
...
	void print() noexcept(true)
	{
		for (auto& it : *(m_data.get()))
		{
			std::cout << it << std::endl;
		}
	}

	void setData(std::initializer_list<T> il) 
		noexcept(noexcept(std::make_shared<std::vector<T>>(il)))
	{
		m_data = std::make_shared<std::vector<T>>(il);
	}
...
};

void testException()
{
	CTestExcept<std::string> obj = {"123", "2222"};
	obj.print();

	auto il = { std::string("3333"), std::string("2222") };
	std::cout << std::boolalpha 
		<< noexcept(obj.print()) << "\n"    //< true
		<< noexcept(obj.setData(il))        //< false
		<< std::noboolalpha << std::endl;
}
  1. 当跟在函数参数列表后时,它是异常说明符
  2. 当作为noexcept异常说明的bool实参出现时,它是一个运算符

异常说明域指针、虚函数和拷贝控制

  1. 函数指针所指的函数必须有一致的异常说明
    1. 函数指针声明noexcept(true),对应函数noexcept(true)
    2. 函数指针声明noexcept(false),对应函数noexcept(true)/noexcept(false)
  2. 如果一个虚函数承诺了它不会抛出异常,这后续派生类的虚函数也必须做出同样的承诺;反之,对派生类的对应函数不做限制
  3. 当编译器合成拷贝控制成员时,同时也生成一个异常说明。如果所有成员和其类的所有操作都承诺了不会抛出异常,这合成的成员是noexcept;如果自定义的析构函数没有提供异常声明,编译将合成一个。
	void (CTestExcept<std::string>:: *lpPrint)() = &CTestExcept<std::string>::print;
	std::cout << typeid(lpPrint).name() << std::endl;
	(obj.*lpPrint)();

// 	void (CTestExcept<std::string>:: * lpSetData)(std::initializer_list<std::string>) noexcept
// 		= &CTestExcept<std::string>::setData;	//< 报错
	void (CTestExcept<std::string>:: * lpSetData)(std::initializer_list<std::string>)
		= &CTestExcept<std::string>::setData;
	std::cout << typeid(lpSetData).name() << std::endl;
	(obj.*lpSetData)(il);

输出

void (__cdecl CTestExcept<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > >::*)(void) __ptr64
123
2222
void (__cdecl CTestExcept<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > >::*)(class std::initializer_list<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > >) __ptr64
请按任意键继续. . .

1.4 异常类层次

异常基类,what()虚函数,获取异常描述
exception

四大类异常,其他异常都应该归类到以下异常分类
bad_cast
runtime_error
logic_error
bad_alloc

runtime_error派生类
overflow_error
underflow_error
range_error

logic_error派生类
domain_error
invalid_error
out_of_range
length_error

2. 命名空间

命名空间为防止名字冲突提供了更加可控的机制

namespace cpp_primer
{
	...
}

2.1 命名空间分类

全局命名空间

全局作用域中定义的名字。全局命名空间以隐式的方式声明,并且所有程序中都存在。

嵌套命名空间

定义在其他命名空间的命名空间

内联命名空间

C++11新的嵌套命名空间。内联命名空间的名字可以被外层命名空间直接使用,无需添加内联命名空间的前缀。

namespace cpp_primer
{
	namespace pub_func
	{
		int add(int a, int b)
		{
			return a + b;
		}
	}

	inline namespace test_func
	{
		bool testAdd()
		{
			if (pub_func::add(1, 2) != 3)
			{
				return false;
			}
		}
	}

	namespace test_func2
	{
		bool testAdd()
		{
			if (pub_func::add(1, 2) != 3)
			{
				return false;
			}
		}
	}
}

void testNamespace()
{
	std::cout << cpp_primer::testAdd() << std::endl
		<< cpp_primer::test_func::testAdd() << std::endl
		//<< cpp_primer::add(1, 2) << std::endl	//< 报错
		<< cpp_primer::pub_func::add(1, 2) << std::endl;
}

内联命名空间的作用:如上代码,如果想启用test_fun2,直接将test_func2定义为内联,关闭test_func的定义即可。方便代码版本切换。或者也可以通过完整的作用域声明,精确指定,需要哪个版本的代码。

匿名的命名空间

	namespace
	{
		int g_flag = 0;
	}

未命名的命名空间中定义的变量拥有静态生命周期,替代静态全局变量

  1. 不能横跨多个不同的源文件
  2. 头文件定义了未命名的命名空间,则该命名空间定义的名字将在每个包含了该头文件的源文件中对应不同的实体

命名空间别名

namespace primer = cpp_primer;

namespace primer = cpp_primer;
std::cout << primer::testAdd() << std::endl
	<< primer::test_func::testAdd() << std::endl
	//<< primer::add(1, 2) << std::endl	//< 报错
	<< primer::pub_func::add(1, 2) << std::endl;

2.2 类、命名空间与作用域

对命名空间内部名字的查找遵循常规的查找规则,即由内外依次查找每个外层作用域。

实参相关的查找与类类型形参

std::string strMsg;
std::cin >> strMsg;
operator>>(std::cin, strMsg);

以上,std:cin >>,实际是调用了 std::operator>>,但并不会写成 std::cin std::>>,也不会写成std::operator。原因是命名空间查找的例外规则:

当我们给函数传递一个类类型的对象(包括指针、引用)时,除了在常规的作用域查找外,还会查找实参所属的命名空间。

友元声明与实参相关的查找
类中声明友元,并不会使友元可见,比如改变原有友元函数的命名空间。但当遇到“实参相关查找命名规则”时,所声明的友元会被认为是当前类的外层命名命名空间的成员。

namespace cpp_class
{
	class CTestNamespace
	{
        //! 使用了 cpp_class::CTestNamespace 对象作为参数
        //! func 仅此时认为是命名空间 cpp_class 的成员    
        //! func 同时还是命名空间 cpp_primer 的成员
		friend void func(cpp_class::CTestNamespace& obj);

        //! 未使用特定参数,func2的所属命名空间为 cpp_primer
		friend void func2();
	};
}

namespace cpp_primer
{
	class cpp_class::CTestNamespace;
	void func(cpp_class::CTestNamespace& obj)
	{
		printf("%s, %d\r\n", __FILE__, __LINE__);
	}

	void func2()
	{
		printf("%s, %d\r\n", __FILE__, __LINE__);
	}
}

void testNamespace()
{
	//! 测试友元
	cpp_class::CTestNamespace obj;
	func(obj);                //< 正常调用,从cpp_class中找到func
	cpp_primer::func(obj);    //< 也能调用成功
//	cpp_class::func(obj);     //< 错误,显示调用不成立
// 	func2();                  //< 错误,需要指定命名空间 cpp_class
// 	cpp_class::func2();       //< 错误,需要指定命名空间 cpp_class
	cpp_primer::func2();      //< 正确
}

2.3 命名空间与重载

如果在不同命名空间出现相同的函数,非冲突时,会出现重载判定。以下几种情况,会影响命名冲突、重载判定的候选集:

  1. 实参命名空间查找,会将对应“实参”命名空间加入候选集
  2. using声明,如:using NS::print,会将print的所有版本,加入当前作用域
  3. using指示,如:using namespace cpp_primer,会将cpp_primer所有命名引入当前作用域
  4. 多个using指示,如:using namespace A,using namespace B,则将AB两个命名空间的函数都引入当前作用域

使用规范:尽量不要使用using,而使用完整地命名空间链。

3. 多重继承 

C++是允许多重继承的,但是多重继承也是极其复杂的语法,建议按Java的继承方案来,即最多只继承一个实体类,其他要继承的必须是接口类(无构造、析构、数据成员),可以规避大多数问题。

3.1 构造函数

构造

在C++11新标准中,允许派生类从它的一个或多个基类中继承构造函数。但是,如果从多个基类中继承了相同的构造函数(即形参列表完全相同),程序将产生错误。

如果一个类从它的多个基类中继承了相同的构造函数,则这个类必须为该构造函数定义它自己的版本。

拷贝和移动

多重继承地派生类如果定义了自己的拷贝控制成员,则必须在完整地对象上执行拷贝、移动活赋值操作。

只有当拷贝等函数是合成的,每个基类才会用合成的成员隐式的完成操作。

3.2 多重继承下的类作用域

在多重继承的情况下,相同的查找过程会在所有基类中同时进行。

当派生类从多个基类继承同名成员的情况,此时,不加前缀限定符(类名::)直接调用该名字,将引发二义性错误。

3.3 虚继承

背景:尽管在派生列表中同一个基类只能出现一次,但实际上派生类可以多次继承同一个类。派生类可以通过它的两个直接基类分别继承同一个间接基类,也可以直接继承某个基类,然后通过另一个基类再一次间接继承该类。

问题:在默认情况下,派生类中含有继承链上每个类对应的子部分。如果某个类在派生后派生过程出现了多次,则派生类中将包含该类多个子对象。

虚继承:共享基类子对象。令某个类做出声明,承诺愿意共享它的基类。

class CZooAnimal
{
public:
	int m_nWeight;
};

class CBear : public virtual CZooAnimal
{
};

class CRaccoon : virtual public CZooAnimal
{
};

class CEndangored
{
public:
	int m_nLevel;
};

class CPanda : public CBear, public CRaccoon, public CEndangored
{
};

虚继承语法:

class CBear : public virtual CZooAnimal

或者 class CRaccoon : virtual public CZooAnimal

虚继承后,CPanda 的 m_nWeight 成员将只有一个。

虚基类成员的可见性-二义性问题

假定 类D1、类D2 继承 类B,类E同时继承类D1,类D2,而类B中有成员X,图示。

此时当E使用到X时,其特性取决于类D1、D2中是否定义有X。

D1 是否有XD2 是否有XE使用的X来自

B.X
D1.X
D2.X
二义性错误

构造函数与虚继承

问题:当以普通规则处理初始化任务时,虚基类会在多条继承路径上重复初始化。

虚继承对象的构造方案:

  1. 使用提供给最底层派生类构造函数的初始值,初始化该对象的虚基类子部分
  2. 按照直接基类在派生列表中出现的次序依次对其进行初始化

虚基类总是先于非虚基类构造,与它们在继承中的次序和位置无关。

特殊工具与技术

1. 控制内存分配

1.1 重载new和delete

这种技术一般用来定义内存泄漏、内存越界问题,new的时候多new个内存、记录new的堆栈,delete时检查多的内存有没被改写,判定是否内存越界;程序退出时,检查是否所有new的块都被释放,未被释放的报内存泄漏。

注意:重载new和delete并没有重载new、delete表达式,而是改变表达式中调用的operator new/delete函数,以改变内存分配的方式。

new表达式的步骤

  1. 调用operator new(或operator new[])分配一块足够大的、原始的、未命名的内存空间
  2. 编译器运行相应的构造函数,构造对象,并为其传入初始值
  3. 对象被分配空间并构造完成,返回一个指向该对象的指针

delete表达式的步骤

  1. 调用对象或数组所有元素对应的析构函数
  2. 调用operator delete(operator delete[])标准库函数释放内存

C++支持提供i新的operator new/delete来改变new/delete的内存分配方式,但不能改变new/delete运算符的基本含义。

应用程序可以在全局作用域、类中定义 operator new/delete函数,调用new/delete时,编译器按如下顺序查找函数:

  1. 类成员(new/delete类类型对象)
  2. 基类(new/delete类类型对象)
  3. 全局(用户自定义函数)
  4. 标准库

可供重载的operator new/delete 函数

  1. void* operator new(size_t _Size);
  2. void* operator new(size_t _Size, ::std::nothrow_t const&) noexcept;
  3. void* operator new[](size_t _Size);
  4. void* operator new[](size_t _Size, ::std::nothrow_t const&) noexcept;
  5. void operator delete(void* _Block) noexcept;
  6. void operator delete(void* _Block, ::std::nothrow_t const&) noexcept;
  7. void operator delete[](void* _Block) noexcept;
  8. void operator delete[](void* _Block, ::std::nothrow_t const&) noexcept;

应用程序可以自定义上面函数版本的任意一个,当作为成员时,new/delete是静态的(可以不声明为static),且无法操作类的任何数据成员。

不允许被重载的operator new函数(标准库内部使用,不创建内存,仅返回指针)

  1. void* operator new(size_t _Size, void* _Where) noexcept
  2. void* operator new[](size_t _Size, oid* _Where) noexcept

案例

//! 记录内存申请和delete数量
int g_allocCount = 0;
void* operator new(size_t _Size)
{
	++g_allocCount;
	void* p = malloc(_Size);
	return p;
}

void operator delete(void* _Block) noexcept
{
	--g_allocCount;
	free(_Block);
}

void testNewDelete()
{
	int* p = new int();
	printf("block count: %d\r\n", g_allocCount);
	delete p;
	printf("block count: %d\r\n", g_allocCount);
	p = new int();
	printf("block count: %d\r\n", g_allocCount);
	p = new int();
	printf("block count: %d\r\n", g_allocCount);
	delete p;
	printf("block count: %d\r\n", g_allocCount);
}

输出

block count: 1
block count: 0
block count: 1
block count: 2
block count: 1
请按任意键继续. . .

1.2 定位new表达式

使用标准的operator new/delete,只负责分配或释放内存变量,类似allocate/deallocate,不会构造/析构对象。不同的是,operator new创建的内存,无法使用construct函数构造函数,只能使用new的定位new形式构造对象。

定位new表达式,定义:在由operator new创建的内存地址上,初始化对象

  1. new (place_address) type
  2. new (place_address) type(initiallizer)
  3. new (place_address) type[size]
  4. new (place_address) type[size]{brazed...}

place_address: 内存地址

type: 初始化的对象类型

initiallizer: 初始值

brazed...:初始化列表

案例

void testPlaceNew()
{
	auto lpStr = (std::string *)operator new(sizeof(std::string));
	new (lpStr) std::string("123");
	printf("%s\r\n", lpStr->c_str());
	lpStr->std::string::~string();
	operator delete(lpStr);
}

2. 运行时类型标识

RTTZ(Run Time Type Identification),两个运算符

运算符说明
typeid判定类型

dynamic_cast<type *>(e):指针

dynamic_cast<type &>(e):引用,左值

dynamic_cast<type &&>(e):引用,右值

类型转换

dynamic_cast<type xx>(e)

当e是类类型时,转换成功的条件

  1. e的类型是目标type的公有派生类
  2. e的类型是目标type的公有基类
  3. e的类型就是目标type类型

其他情况,转换失败,如果是指针,返回0;如果是引用,返回bad_cast异常。

typedid(e)

e:任意表达式或类型的名字

返回:常量对象的引用,是type_info或type_info的公有派生类

  1. 顶层const被忽略
  2. 如果e是引用,后返回e所引用对象的类型
  3. 如果是数组,返回数组类型,不执行指针的标准类型转换
  4. 如果是指针,返回指针静态编译时类型
  5. 当 e!=类类型,或e不包含任何函数的类,返回e的静态类型
  6. 当e定义了至少一个虚函数的类的左值,返回e实际指向的对象的类型(运行时阶段)。注意:需要使用*bp,作用于对象。
  7. 如果p是空指针,抛出bad_typeid异常

应用场景,比较两个类类型(派生类 or 基类)是否一致

问题:子类和父类对象,进行判定时,理论上不相等,当使用虚函数时,由于基类内容一致,可能会导致相等的结果。

方案:判断相等前,用typeid相判定类型

class A
{
public:
	virtual bool equal(A &obj)
	{
		return obj.m_nData == this->m_nData;
	}
private:
	int m_nData = 0;
};

class B : public A
{
public:
	virtual bool equal(A& obj) override
	{
		return typeid(obj) == typeid(*this) 
			&& dynamic_cast<B&>(obj).m_strData == this->m_strData;
	}
private:
	std::string m_strData = "123";
};

//! 判定函数
A obj1;
B obj2, obj3;
printf("obj1 == obj2, %d\r\n", obj1.equal(obj2));
printf("obj2 == obj1, %d\r\n", obj2.equal(obj1));
printf("obj2 == obj3, %d\r\n", obj2.equal(obj3));

3. 枚举类型

分为:不限定作用域类型(旧标准),限定作用域类型(C++11新标准)

不限定作用域:enum color {} 或 未命名 enum {},可以隐式转换成整型

限定作用域:enum class color {} 等价于 enum struct color {},不能隐式转换成整型

	enum class NEW_COLOR
	{
		NEW_RED,
		NEW_GREEN
	};

	enum OLD_COLOR
	{
		OLD_RED,
		OLD_GREEN
	};

	//! 可以隐式转换为整型
	int nColor = OLD_RED;
	//! 不能隐式转换,必须指定作用域
	nColor = (int)NEW_COLOR::NEW_RED;
	NEW_COLOR emColor = NEW_COLOR::NEW_RED;

指定enum大小

enum class NEW_COLOR : unsigned long long

enum OLD_COLOR : unsigned long long

可以通过 ": 类型",指定枚举成员类型,而不是默认的int

enum class NEW_COLOR : unsigned long long
{
	NEW_RED,
	NEW_GREEN,
	NEW_GRAY = ULLONG_MAX - 1
};

enum OLD_COLOR : unsigned long long
{
	OLD_RED,
	OLD_GREEN,
	OLD_GRAY = ULLONG_MAX - 1
};

//! 可以隐式转换为整型
unsigned long long nColor = OLD_GRAY;
printf("color: %llu\r\n", nColor);
OLD_COLOR emOldColor = OLD_GRAY;
printf("color: %llu\r\n", emOldColor);

//! 不能隐式转换,必须指定作用域
nColor = (int)NEW_COLOR::NEW_GRAY;
printf("color: %llu\r\n", nColor);
NEW_COLOR emNewColor = NEW_COLOR::NEW_GRAY;
printf("color: %llu\r\n", emNewColor);

枚举类型的前置声明

C++11新标准,可以和类类型一样,前置声明enum,声明和定义必须匹配

enum class NEW_COLOR : unsigned long long;
enum OLD_COLOR : unsigned long long;
void printEnum(NEW_COLOR &emNew, OLD_COLOR &emOld)
{
	printf("New: %llu, old: %llu\r\n", emNew, emOld);
}

4. 类成员指针

即可以指向类的非静态成员(数据成员、成员函数)的指针。

4.1 成员数据指针

定义:const std::string Screen::*pData;

说明:指向Screen类的const std::string成员的指针。注意:Screen指定类,* 指定指针类型。

赋值:pData = &Screen::m_strData;

使用:obj.*pData; obj为类对象,或者pObj->*pData使用功能类指针。

	Screen obj;
	const std::string Screen::* pData;
	pData = &Screen::m_strData;
	printf("%s\r\n", (obj.*pData).c_str());

4.2 成员函数指针

定义:const std::string(Screen:: * lpMemFunc)() const = &Screen::getData;

说明:定义了lpMemFunc指向Screen::getData成员,lpMemFunc同普通函数指针一致,就是多了Screen::类作用域声明。注意,普通函数指针,尾部不允许声明const,而成员函数需要保持一致。

使用:std::string strMsg = (obj.*lpMemFunc)(); 同数据成员一样,需要类对象来调用。注意调用时不能缺少(),由于调用符运算优先级,高于指针指向成员的运算符的优先级。

class Screen
{
public:
	const std::string getData() const
	{
		return m_strData;
	}

	std::string m_strData = "123";
};

const std::string(Screen:: * lpMemFunc)() const = &Screen::getData;
std::string strMsg = (obj.*lpMemFunc)();

4.3 将成员函数作为可调用对象

定义:std::function<bool(const std::string&)> fcn = &std::string::empty;

说明:成员函数,第一个参数是隐式的类的 this 指针,所以定义function时,传入 std::stirng &

调用:fcn(str)

std::vector<std::string> vctStr = { "123", "", "222", "" };
std::function<bool(const std::string&)> fcn = &std::string::empty;
int nCount = 0;
for (auto it : vctStr)
{
	if (fcn(it))
	{
		++nCount;
	}
}
printf("Empty string count: %d\r\n", nCount);

使用mem_fn简化生成可调用对象

auto fcn = std::mem_fn(&std::string::empty);

使用bind生成可调用对象

auto fcn = std::bind(&std::string::empty, std::placeholders::_1);

5. union

一种特殊的类类型,在嵌入式、底层比较常用的类型,应用层用的不多。

主要特性,union大小为最大成员的大小,且所有成员共享同一块内存。

原本union只能存储内置类型,但C++11新增了特性,允许存储类类型成员,导致其复杂度高了很多。

和普通类的区别

  1. 不能带有引用类型的成员
  2. 默认为public,同struct
  3. 不能继承、派生,不能有虚函数
  4. C++11新特性,含有构造或析构类型的类对象,也可以作为union的成员类型

当union仅包含内置类型,编译器合成默认构造函数或拷贝控制成员。但入股union包含类类型成员,则合成对应版本的构造、析构被声明为删除,需要执行定义,否则union对象无法创建。

当包含类类型的union,如果给类类型赋值了,其他成员的值将失去意义。使用起来也相当复杂:

  1. 1. 给类类型赋值时,如果原先赋值的不是类类型,需要使用new定位式构建类类型对象
  2. 2. 当给其他成员赋值,而当前时类类型时,需要主动调用析构函数,析构当前类类型对象
  3. 3. 当前是类类型成员对象有值,而要销毁union对象时(无论是delete还是局部变量回收),都需要提前主动调用析构,销毁类类型成员。

因此,以上,还需记录下当前到底给哪个成员赋值。

6. 位域

类可以将其非静态的数据成员定义成位域,一个位域中含有一定数量的二进制位。

class FileHead
{
public:
	int mode : 2;	//< mod占2位
	int modified : 1;	//< modified占1位
};

7. volatile限定符

就是避免编译器优化,如下场景

bool g_flag = true;
unsigned ThreadFunc(void* pArguments)
{
    while(g_flag)
    {
    }
return 0;
}

多线程访问g_flag,此时while(g_flag)由于编译优化,会直接变成while(true),导致多线程死循环。

而声明为volatile,避免编译器优化。

注意:不能将一个非volatile引用绑定到一个volatile对象上

8. 链接指示extern "C"

C++使用链接指示指出任意非C++函数所用的预言。

形式:单个指定 extern "C" xxx。复合指定 extern "C" { xxx }。

不能出现在类定义或函数定义的内部,同样的链接指示必须在函数的每个声明中出现。

指向extern "C" 函数的指针:

指向其他语言编写的函数的指针必须与函数本身使用相同的链接指示。

extern "C" void (*pf)(int)

  • 14
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

求知向道

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

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

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

打赏作者

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

抵扣说明:

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

余额充值