第3章 转向现代C
条款7:在创建对象时注意区分()和{}
//创建对象时候注意区分 () 和 {}
//指定初始化的方式有:小括号,等号,大括号
//情况1:内建型别来说 int 初始化和赋值没有区别
代码语言:javascript复制int x(1);
int y = 2;
int z{3};
int zz = {4};//等号 {}也是可以的
//对于用户定义的型别 初始化和赋值区别大了
代码语言:javascript复制//对于用户定义的型别 初始化和赋值区别大了
class Widget{
public:
// Widget();
// Widget(const Widget &w);
// Widget& operator=(const Widget &w);
};
Widget w1;//调用的是默认构造函数
Widget w2 = w1;//并非赋值,调用的是复制构造函数
//w1 = w2;//并非赋值,调用的是复制赋值运算符
//普遍性:大括号初始化
//1, STL容器
代码语言:javascript复制std::vector<int> v{1,3,5};
//2:非静态成员指定默认初始化值 或者 等号,但是不能使用小括号
代码语言:javascript复制class WidgetA{
public:
WidgetA(int a) {}
WidgetA() {}
int x{5};//可行
int y = 6;//也可行
//int z(0);//不可行
};
//3: 不可复制的对象,可以用 {}或()进行初始化,不能使用 =
代码语言:javascript复制std::atomic<int> ai1{0};
std::atomic<int> ai2(0);
//std::atomic<int> ai3 = 0;//不可行
//结论:可见 {} 初始化的方式比较统一,但是注意它有一项新特性:
//禁止内建型别之间进行隐式型别转换,如果大括号内的表达式无法保证能够采用进行初始化的对象来表达,则代码不能通过编译
代码语言:javascript复制double x1,y1,z1;
//int sum1{x1 y1 z1};//错误,double型别之后可能无法用 int 来表达
//改进:采用 () 和 = 的初始化不会进行型别转换检查,可以编译通过
代码语言:javascript复制int sum2(x y z);
int sum3 = x y z;
//大括号解决的第二类问题:最令人苦恼之解析语法
//C 规定:任何能够解析为声明的都要解析为声明,这就跟默认构造造成了冲突,变成了声明一个函数
代码语言:javascript复制WidgetA w11(10);//构造函数
WidgetA w22();//调用一个没有形参的构造函数,结果变成声明一个函数而非对象
WidgetA w33{};//函数形参不能使用大括号来指定形参列表,所有使用大括号来完成对象的默认构造没有问题
//大括号解决的第三类问题:构造函数形参中 具备 std::initializer_list型别
//1, 如果没有以上型别,() 和 {} 没有区别
代码语言:javascript复制class WidgetB{
public:
WidgetB(int i, bool b){}
WidgetB(int i, double d){}
};
WidgetB w111(10,true);//第一个
WidgetB w222{10,true};//第一个
WidgetB W333(10,5.0);//第二个
WidgetB W444{10,5.0};//第二个
//2, 如果一个或多个构造函数声明了任何一个具备 std::initializer_list型别的形参
//那么 {} 会强烈优先选择带有这个形参的重载版本
代码语言:javascript复制class WidgetBB{
public:
WidgetBB(int i, bool b){}
WidgetBB(int i, double d){}
WidgetBB(const WidgetBB& w){}
WidgetBB(std::initializer_list<long double> i1){}
//bool和std::string并没有强制类型转换函数,优先调用默认构造
WidgetBB(std::initializer_list<bool> i1){}
WidgetBB(std::initializer_list<std::string> i1){}
//即使是平常会执行 复制或移动的构造函数也可能被 std::initializer_list 型别形参劫持
operator float() const; //强制转换成 float 型别
};
WidgetBB w111(10,true);//第一个
WidgetBB w222{10,true};//第三个, 强制转换
WidgetBB W333(10,5.0);//第二个
WidgetBB W444{10,5.0};//第三个,强制转换
// WidgetBB w5(w444);//小括号 调用的是 复制构造函数
// WidgetBB w6{w444};//大括号,调用的是带有 std::initializer_list型别形参的构造函数,w4的返回值被强制转成成 float,随后 float又被强制转成了 long double
// WidgetBB w7(std::move(W444));//小括号,移动构造
// WidgetBB w8{std::move(W444)};//大括号, 利用同上
//3, 没有实参,应该执行默认构造
代码语言:javascript复制class WidgetA1{
public:
WidgetA1();
WidgetA1(std::initializer_list<int> il);//带有形参的构造
};
WidgetA1 ww;//调用的是默认构造
WidgetA1 WWW{};//依然是默认构造
WidgetA1 WWWW();//变成函数的声明了,令人头疼的语法
//如果的确想调用一个带有 std::initializer_list 型别形参的构造函数,并传入一个空的 std::initializer_list的话
//可以通过把空大括号对作为构造函数实参的方式实现这个目的
WidgetA1 wW11({});//满足以上调用
WidgetA1 wW22{{}};//同上
//以上讲解的实际应用
std::vector<int> v1(10,20);
std::vector<int> v2{10,20};
//4, 开发模板的程序员,创建对象是选择 () 还是 {} 大不相同,可变模板
代码语言:javascript复制//4, 开发模板的程序员,创建对象是选择 () 还是 {} 大不相同,可变模板
template<typename T, //创建对象的型别
typename... Ts> //一系列实参的型别
void doSomeWork(Ts&&... params)
{
//利用params 创建局部对象 T
T localObject(std::forward<Ts>(params)...); //采用 ()
T localObject{std::forward<Ts>(params)...};//采用 {}
};
//调用代码
std::vector<int> v;
//dosomework中使用小括号,得到得到一个包含10 个元素的 std::vector
//dosomework中使用大括号,得到一个包含 2 个元素的 std::vector
doSomeWork<std::vector<int>>(10,20);
测试端
代码语言:javascript复制int main()
{
cout<<"XXXX: "<<x<<" "<<y<<" "<<z<<" "<<zz<<endl;
WidgetA *ww = new WidgetA();
cout<<"WWWW: "<<ww->x<<" "<<ww->y<<endl;
cout<<"WWWWX: "<<ai1<<" "<<ai2<<" "<<endl;
}
// 要点速记
// 大括号初始化可以应用的语境最为宽泛,可以阻止隐式窄化型别转换,还对最令人苦恼之觥析语法免疫 。
// 在构造函数重载决议期间,只要有任何可能,大括号初始化物就会与带有std: : initializer_ list 型别的形参相匹配,即使其他重载版本有着貌似更 加匹配的形参表 。
// 使用小括号还是大括号,会造成结果大相径庭的一个例子是:使用两个实参来创建一个 std: : vector< 数值型别>对象 。
// 在模板内容进行对象创这时,到底应该使用小括号还是大括号会成为一个 棘手问题。
条款8:优先选用nullptr,而非0或NULL
// 0 的型别是 int, 0 和 NULL 都不具备指针型别
//情况1:重载函数
代码语言:javascript复制//情况1: 重载函数
void f(int);//f 的三个重载版本
void f(bool);
void f(void*);
f(0);//调用的是 f(int),而不是 f(void*)
f(NULL);//可能通不过编译,但一般会调用 f(int),从来不会调用f(void*)
//nullptr 的优点是,它不具备整型型别,也不具备指针型别,但你可以把它想成一种任意型别的指针
f(nullptr);//调用 f(void*)这个重载版本
//情况2:auto 定义中提升代码的清晰性
代码语言:javascript复制//情况2:auto 定义中提升代码的清晰性
auto result = findRecord(12);
if(result == 0){
//不知道findRecord的返回值型别的话,result是指针型别还是整数型别就不清楚了
//0 作为结果值两者都有可能
}
if(result == nullptr){
//result必然具备指针型别
}
//情况3:模板函数中 nullptr更具优势
//适当的信息量被锁定才调用,每个函数的形参都是不同型别的指针
代码语言:javascript复制class Widget{
};
int f1(std::shared_ptr<Widget> spw);
double f2(std::unique_ptr<Widget> upw);
bool f3(Widget* pw);
//调用这些函数并传入空指针
std::mutex f1m, f2m,f3m;
using MuxGuard = std::lock_guard<std::mutex>;
{
MuxGuard g(f1m);//为f1 锁定互斥量
auto result = f1(0);//向 f1传入 0 作为空指针
//解锁互斥量
}
{
MuxGuard g(f2m);//为f2 锁定互斥量
auto result = f2(NULL);//向 f2传入 NULL 作为空指针
//解锁互斥量
}
{
MuxGuard g(f3m);//为f3 锁定互斥量
auto result = f1(0);//向 f3传入 nullptr 作为空指针
//解锁互斥量
}
//调用代码的三步曲(锁定互斥量,调用函数,解锁信号)冗余,改成模板
代码语言:javascript复制template<typename FuncType, typename MuxType, typename PtrType>
auto lockAndCall(FuncType func, MuxType& mutex, PtrType ptr) -> decltype(func(ptr))//C 11
//decltype(auto) lockAndCall(FuncType func, MuxType& mutex, PtrType ptr);//c 14
{
MuxGuard g(mutex);
return func(ptr);
}
//调用代码如下
auto result1 = lockAndCall(f1,f1m,0);//错误 0 的型别是 int,不是智能指针, 把 int 传给了一个 std::shared_ptr<Widget>
auto result2 = lockAndCall(f2,f2m,NULL);//错误
auto result3 = lockAndCall(f3,f3m,nullptr);//没问题
//要点速记
//1, 相对于 0 或 NULL,优先选用 nullptr
//2, 避免在整型和指针型别之间重载
条款9:优先选用别名声明,而非typedef
//毛病:std::unique_ptr<std::unordered_mapstd::string,std::string >
代码语言:javascript复制//方法一:typedef
typedef std::unique_ptr<std::unordered_map<std::string,std::string> > UPtrMapSS;
//方法二:using
using UPtrMapSS = std::unique_ptr<std::unordered_map<std::string,std::string> >
//既然以上两种方法都可以实现简化,有什么区别呢?
//区别一:处理函数指针的理解性
代码语言:javascript复制//区别一:处理函数指针的理解性
//FP的型别是一个指涉到函数的指针,该函数形参包括一个 int 和一个 const std::string&, 没有返回值
typedef void (*FP)(int, const std::string&);
using FP = void(*)(int,const std::string&);
//区别二:using声明可以模板化,typedef就不行
代码语言:javascript复制//区别二:using声明可以模板化,typedef就不行
//定义一个同义词,表达一个链表,使用了一个自定义分配器 MyAlloc
//MyAllocList<T> 是 std::list<T,MyAlloc<T>>的同义词
class Widget{
};
template<typename T>
using MyAllocList = std::list<T,MyAlloc<T>>;
MyAllocList<Widget> lw;
//使用typedef,从头开始写起
//MyAllocList<T>::type是 std::list<T,MyAlloc<T>>的同义词
template<typename T>
struct MyAllocList{
typedef std::list<T,MyAlloc<T>> type;
};
MyAllocList<Widget>::type lw;
//如果你想在模板内使用 typedef来创建一个链表,它容纳的对象型别由模板参数指定的话
//你需要给 typedef 的名字加一个typename前缀
//Widget<T>含有一个 MyAllocList<T>型别的数据成员
template<typename T>
class Widget{
private:
typename MyAllocList<T>::type list;
};
//以上 MyAllocList<T>::type代表一个依赖于模板型别形参 T 的型别,所以 MyAllocList<T>::type
//称为带依赖型别,c 规定带依赖型别必须前面加个 typename
//如使用 using 模板定义,typename的要求就消失了
template<typename T>
using MyAllocList = std::list<T,MyAlloc<T>>;
template<typename T>
class Widget{
private:
MyAllocList<T> list;
};
//MyAllocList<T>是个非依赖型别,所以 typename不要求了
//型别转换:C 11中的变换 std::transformation::type
//都存在一个 C 14中名为 std::transformation_t的对应别名模板
代码语言:javascript复制std::remove_const<T>::type //congst T 生成 T c 11
std::remove_const_t<T> //C 14
std::remove_reference<T>::type // T& 或者 T&& 生成 T c 11
std::remove_reference_t<T> //C 14
std::add_lvalue_reference<T>::type //T 生成 T& c 11
std::add_lvalue_reference_t<T> //c 14
//其实呀,C 14 就是 C 11的 using 声明
代码语言:javascript复制template<class T>
using remove_const_t = typename remove_const<T>::type
template<class T>
using remove_reference_t = typename remove_reference<T>::type
template<class T>
using add_lvalue_reference_t = typename add_lvalue_reference<T>::type
//要点速记
// typedef 不支持模板化 ,但别名声明支持
// 别名模板可以让人免写 “::type” 后缀,并且在模板内,对于内嵌 typedef 的引用经常要求加上 typename前缀
条款10:优先选用限定作用域的枚举型别,而非不限作用域的枚举型别
//通用规则:如果在一对大括号里声明一个名字,则该名字的可见性就被限定在括号括起来的作用域内
//情况1:作用域的不同
//C 98 enum
//枚举量的名字属于包含着这个枚举型别的作用域,意味着在此作用域内不能有其他实体取相同的名字
代码语言:javascript复制//情况1:作用域的不同
//C 98 enum
//枚举量的名字属于包含着这个枚举型别的作用域,意味着在此作用域内不能有其他实体取相同的名字
enum Color{
black,
white,
red
};//black所在作用域和color相同
auto white = false;//错误,white已经在范围内被声明过了
//C 11 enum class 枚举类,避免以上问题
enum class Color{
back,
white,
red
};//black作用域被限定在Color内
auto white = false;//没问题,范围内并无其他的 white
Color c = white;//错误! 范围内并无 white的枚举量
Color c = Color::white;//没问题
auto c = Color::white; //没问题
//情况2:限定作用域的枚举量是更强型别的,不限范围的枚举型别中的枚举量可以隐式转换到整数型别
//并且能够进一步转换到浮点型别
//不限范围的枚举型别
代码语言:javascript复制//情况2:限定作用域的枚举量是更强型别的,不限范围的枚举型别中的枚举量可以隐式转换到整数型别
//并且能够进一步转换到浮点型别
//不限范围的枚举型别
enum Color{
black,
white,
red
};
//函数,返回x的质因数
std::vector<std::size_t> primeFactors(std::size_t x);
Color c = red;
//将Color型别和double型别值比价
if(c < 14.5){
//计算一个Color型别的质因数
auto factors = primeFactors(c);
}
//限定作用域的枚举型别,只要加个 class,就不存在隐式转换路径
代码语言:javascript复制//限定作用域的枚举型别,只要加个 class,就不存在隐式转换路径
enum class Color{
black,
white,
red
};
Color c = Color::red;//加上范围限定修饰词
//错误! 不能将Color型别和double型别值比价
if(c < 14.5){
//错误!不能将Color型别传入,要求 std::size_t型别形参的函数
auto factors = primeFactors(c);
}
//强制型别转换
代码语言:javascript复制//强制型别转换
if(static_cast<double>(c) < 14.5){
auto factors = primeFactors(static_cast<std::size_t>(c));
}
//情况3:限定作用域的枚举型别可以进行前置声明,其型别名字可以比其中的枚举量先声明
//C 98 这样规定,但是C 11 可以了
代码语言:javascript复制//C 98 这样规定,但是C 11 可以了
enum Color;//错误!
enum class Color;//没问题
//C 11:可以上述声明,一切枚举型别在 C 里都会由编译器来选择一个整数型别作为其底层型别
//编译器通常会为枚举型别选用足够表示枚举量取值的最小底层型别。
代码语言:javascript复制//char作为底层型别
enum Color{
black,
white,
red
};
//char范围不够,选择int型别
enum Status{
good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
indeterminate = 0xFFFFFFFF
};
//如若突然在 Status里增加一个 audited =500, 则需要重新进行编译
//改进:C 11中为枚举型别提供的前置声明能力可以破除重新编译的麻烦
代码语言:javascript复制//前置声明
enum class Status;
//取用前置声明的枚举型别
void continueProcessing(status s);
//即使Staus被修改了,也无需重新编译
//说到这里,为什么C 11中的枚举型别可以进行前置声明,而C 98中就不行呢?
//答案:限定作用域的枚举型别的底层型别式已知的,而对于不限范围的枚举型别,你可以指定这个底层型别
代码语言:javascript复制//默认地,限定作用域地枚举型别的底层型别式 int
//int
enum class Status;
//可以自由改动默认型别
enum class Status std::uint32_t;
//同样:也可以指定不限范围的枚举型别,这样以后,它也可以进行前置声明了
代码语言:javascript复制enum Color: std::uint8_t;
//底层型别指定同样也可以在枚举型别定义中进行
enum class Status: std::uint32_t{
good = 0.
failed = 1,
...
indeterminate = 0xFFFFFFFFF
};
//不限作用域的枚举型别的应用:难道无用武之地了吗?
代码语言:javascript复制//引用C 11 中的 std::tuple型别的各个阈时,假设为一个社交网站准备一个元组来持有名字,电子邮件和声望值
using UserInfo = std::tuple<std::string, std::string, std::size_t>;
//应用
UserInfo uInfo;//std::tuple型别对象
auto val = std::get<1>(uInfo);//取用阈 1的值
//需要记得 阈1 对应的式什么?
//换一种方式
代码语言:javascript复制enum UserInfoFields{
uiName,
uiEmail,
uiReputation
};
UserInfo uInfo;
auto val = std::get<uiEmail>(uInfo);//取得电子邮件对应的阈了
//以上代码依赖于 UserInfoFields向 std::size_t的隐式型别转换,转换结果就是 std::get要求的型别
//而采用限制作用域的枚举型别就啰嗦多了
代码语言:javascript复制enum class UserInfoFields{
uiName,
uiEmail,
uiReputation
};
UserInfo uInfo;
auto val = std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>(uInfo);//取得电子邮件对应的阈了
//以上可以转换成一个函数
//std::get是个模板,传入的值是一个模板形参,所以这个将枚举量变换成 std::size_t型别值得函数必须在编译期就要计算出结果
//意味着必须使用 constexpr 函数
代码语言:javascript复制//C 11
template<typename E>
constexpr typename std::underlying_type<E>::type
toUType(E enumerator) noexcept
{
return static_cast<typename std::underlying_type<E>::type>(enumerator);
}
//C 14中时髦写法
代码语言:javascript复制//C 14中时髦写法
template<typename E>
constexpr typename std::underlying_type_t<E> //auto 返回值也行
toUType(E enumerator) noexcept
{
return static_cast<typename std::underlying_type_t<E>>(enumerator);
}
//访问元组
代码语言:javascript复制//访问元组
auto val = std::get<toUType(UserInfoFields::uiEmail)>(uInfo);
要点速记
• C 98 风格的枚举型别,现在称为不限范围的枚举型别
• 限定作用域的枚举型别仅在枚举型别内可见 它们只能通过强制型别转换以转换至其他型别。
• 限定作用域的枚举型别和不限范围的枚举型别都支持底层型别指定。限定作用成的枚举型别的默认底层型别是 int, 而不限范围的枚举型别没有默认底层型别
• 限定作用域的枚举型别总是可以进行前置声明,而不限范围的枚举型别却只有在指定了默认底层型别的前提下才可以进行前置声明。
条款11:优先选用删除函数,而非private未定义函数
//宗旨:阻止调用函数得方法:函数未经声明,不可调用
//删除函数得优点1:
//删除函数无法通过任何方法使用,所以即使成员和友元函数中得代码也会因试图复制
//basic_ios型别对象而无法工作, 而 private只有在链接阶段才能诊断出来
//C 98中:private阻止客户去调用它们,故意不去定义它们
//意味着一段代码仍然可以访问它们,如成员函数,或类得友元并使用了它们
//链接阶段就会由于缺少定义而失败
//例如:为了让输入流和输出流类成为不可复制得
代码语言:javascript复制//例如:为了让输入流和输出流类成为不可复制得
template<class charT,class traits = char_traits<charT> >
class basic_ios:public ios_base{
public:
...
private:
basic_ios(const basic_ios&);//没有定义
basic_ios& operator=(const basic_ios&);//没有定义
};
//C 11中 delete将复制构造函数和复制赋值运算符标识为删除函数
代码语言:javascript复制//C 11中 delete将复制构造函数和复制赋值运算符标识为删除函数
template<class charT,class traits = char_traits<charT> >
class basic_ios:public ios_base{
public:
...
basic_ios(const basic_ios&) = delete;
basic_ios& operator=(const basic_ios&) = delete;
};
//优点2:任何函数都能成为删除函数,但只有成员函数能声明为 private
//可以凑合看作是数值得型别,都可以隐式转型到 int ,因此阻止调用通过编译得方法
//为我们想要过滤得型别创建删除重载版本
代码语言:javascript复制bool isLucky(int number);//原始版本
bool isLucky(char) = delete;//拒绝char型别
bool isLucky(bool) = delete;//拒绝bool型别
bool isLucky(double) = delete;//拒绝double和float型别
if(isLucky('a'))//错误,调用了删除函数
if(isLucky(true))//错误
if(isLucky(3.5f))//错误
//优点3:删除函数可以阻止那些不应该进行得模板具现,private成员函数做不到
//假设需要一个和内建指针协作得模板
代码语言:javascript复制template<typename T>
void processPointer(T* ptr);
//下面说说指针世界得两个异类
//1,void*指针,无法对其进行自增,自减得操作
//2,char* 指针表示得式C风格得字符串,不是指涉到单个字符得指针
//假定采用这两个型别时拒绝调用,不可以使用 void* 和 char* 来调用 processPointer
//删除来具体实现
代码语言:javascript复制//删除来具体实现
template<>
void processPointer<void>(void*) = delete;
template<>
void processPointer<char>(char*) = delete;
template<>
void processPointer<const void>(const void*) = delete;
template<>
void processPointer<const char>(const char*) = delete;
//不可取得做法,private
//因为你不可能基于成员函数模板得某个特化以不同于主模板得访问层级
//如果 processPointer是在 Widget内部得一个成员函数模板,而你想禁止使用 void*指针来调用
//通不过编译得做法
代码语言:javascript复制//通不过编译得做法
class Widget{
public:
...
template<typename T>
void processPointer(T* ptr)
{...}
private:
template<>
void processPointer<void>(void*);//错误
//模板特化必须在名字空间作用域而不是类作用域内撰写
};
//改用删除函数来实现有两大好处
//1, 它们根本不需要不同得访问层级
//2,因为成员函数模板可以在类外(名字空间作用域)被删除
代码语言:javascript复制class Widget{
public:
...
template<typename T>
void processPointer(T* ptr)
{...}
};
//仍然具备 public访问层级,但是被删除了
template<>
void Widget::processPointer<void>(void*) = delete;
要点速记
• 优先选用删除函数,而非 privat 未定义函数。
• 任何函数都可以删除,包括非成员函数和模板具现。