新手怎么进入现代C++
1. 使用auto
来自动推导变量类型
2. 使用{}
来创建变量和对象
3. 使用nullptr
来创建空指针
4. 使用using
代替typedef
进行别名定义
5. 使用enum class
代替enum
进行枚举定义
6. 使用=delete
来禁止调用一个函数
7. 使用override
来修饰继承链中的重写函数
8. 使用const_iterators
代替iterators
来表示常量迭代器
9. 使用noexcept
来修饰不会抛出异常的函数
10. 使用constexpr
来定义常量表达式
Item 5: Prefer auto
to explicit type declarations
C++11引入了auto
,用来进行自动类型推导,即:编译器会根据变量的初始化表达式来推导出变量的类型。面对这个新特性,在声明和定义一个变量时应该优先考虑auto
。
既然要优先考虑,那使用auto
的好处是啥?
- 避免变量未初始化:在编写C++代码时,建议在声明和定义变量时赋予初值,以避免程序默认初始化出意料之外的值。如果使用
auto
定义变量但未赋初值,编译器会发出警告;
int a = 10;
// 编译器告警:Declaration of variable 'b' with deduced type 'auto' requires an initializer
// auto b;
- 省略一个冗长的类型:当我们在遍历
std::vector
时,可能会使用容器的std::vector<int>::const_iterator
。下面的代码展示了迭代器类型的冗长性,而使用auto
大大简化了这段代码。
std::vector<int> vec{1, 2, 3, 3};
for (std::vector<int>::const_iterator itr = vec.cbegin(); itr != vec.cend(); ++itr) {
printf("%d\n", *itr);
}
for (auto itr = vec.cbegin(); itr != vec.cend(); ++itr) {
printf("%d\n", *itr);
}
- 定义一个
lambda
表达式:对于一个lambda
表达式,我们可能不确定其具体类型,可以使用auto
让编译器来为我们自动推导出类型。
auto func1 = [](int a, int b) { return a + b; };
- 避免由于不清楚C++代码内部逻辑而引发的问题
unsigned
类型在不同位数的操作系统上占用内存大小是不一样的;std::unordered_map
的key是const
的;
/**
* unsigned在win32系统中的大小为32位,在win64系统中的大小为32;
* 但是,vec.size()返回的具体类型是std::vector<int>::size_type
* std::vector<int>::size_type在win32系统中的大小为32位,在win64系统中的大小为64;
*
* 这样会导致的问题是:在win64系统上vec的size超过32位表示后,unsigned vec_size就会有问题了。
*/
std::vector<int> vec{1, 2, 3, 4};
unsigned vec_size = vec.size();
/**
* std::unordered_map的key是const的
* 这个代码中只是说std::pair是const的,但并没有确定std::string是const的;
*
* 这样会导致的问题是:std::string会经历拷贝操作。
*/
std::unordered_map<std::string, int> string_2_int_map;
for (const std::pair<std::string, int> &v : string_2_int_map) {
}
Item 7: Distinguish between () and {} when creating objects
在C++中,有特别多的变量初始化语法,int a = 10;
int a(10);
int a{10};
int a = {10};
对于初学者来说就很混乱(不清楚每一种初始化语法都有啥区别)。
C++11中使用统一初始化(uniform initialization)来整合所有情景的初始化语法,也就是统一使用花括号(大括号)来进行各类初始化。
那统一初始化有哪些优势呢?
- 花括号初始化不允许内置类型的变窄转换;
- 花括号表达式对于C++最令人头疼的解析问题有天生的免疫性(C++规定任何可以被解析为一个声明的东西必须被解析为声明);
- 需要了解某些场景下花括号初始化的行为(主要是涉及到
std::initializer_lis
);
class Sample {
public:
Sample() { printf("Sample() Called\n"); }
Sample(int a) { printf("Sample(int a) Called\n"); }
// Sample(std::initializer_list<double> param) { printf("Sample(std::initializer_list<double> param) Called\n"); }
Sample(std::initializer_list<std::string> param) { printf("Sample(std::initializer_list<std::string> param) Called\n"); }
};
int main() {
/** 1.
* C++11使用统一初始化(uniform initialization)来整合所有情景的初始化语法;
* 统一初始化:是指在任何涉及初始化的地方都使用单一的初始化语法(基于花括号);
*/
int a1 = 10;
int a2(10);
int a3{10};
int a4 = {10};
/** 2.
* 花括号初始化不允许内置类型的变窄转换
*/
double x, y, z;
// 编译器告警: Type 'double' cannot be narrowed to 'int' in initializer list
// int a{x + y + z};
int b(x + y + z); // 存在一个double向int的默认类型转换
int c = x + y + z; // 存在一个double向int的默认类型转换
/** 3.
* 花括号表达式对于C++最令人头疼的解析问题有天生的免疫性;
* C++规定任何可以被解析为一个声明的东西必须被解析为声明;
* 例如:
* 使用小括号()想调用默认构造函数初始化一个对象,结果被编译器解析为函数声明
*/
// 编译器告警:Empty parentheses interpreted as a function declaration(空的括号会被解析为一个函数的声明)
// Sample s1();
Sample s2{}; // 使用花括号可以明确解决该类问题:此处明确为调用Sample的默认构造函数
/** 4.
* 花括号初始化的缺点:有时候会有一些奇怪的行为
* 1.当auto声明的变量使用花括号初始化,变量类型会被推导为std::initializer_list,而其他初始化方式会产生该类型的结果;
* 2.构造函数中,如果存在一个/多个构造函数包含std::initializer_list,花括号的初始化会强制选择带std::initializer_list的构造函数;
* 3.只有当没办法把括号初始化中实参的类型转化为std::initializer_list时,编译器才会回到正常的函数决议流程中;
* 4.使用花括号初始化是空集,并且存在默认构造函数,也存在std::initializer_list构造函数,会调用默认构造函数,说明这个代表没有实参;如果再用一个花括号来作为函数实参,就会调用存在std::initializer_list构造函数;
*/
auto aa1 = {10}; // auto会被推导为std::initializer_list<int>
int aa2 = 10; // aa2就是预期内的类型int
// 编译器告警:从 "int" 到 "long double" 进行收缩转换无效
// Sample ss1{aa2}; // 虽然这个位置最合适的构造函数是Sample(int a),但是依然会调用Sample(std::initializer_list<double> param);
Sample ss2{10}; // int无法转换为std::string,所以调用的构造函数是Sample(int a)
Sample ss3{}; // Sample() Called
Sample ss4{{}}; // Sample(std::initializer_list<std::string> param) Called
/** 5.
* 对于std::vector来说,既存在非std::initializer_list构造函数,也存在std::initializer_list构造函数;
* 所以使用圆括号和花括号的初始化方式的差别会非常大。
*/
std::vector<int> vec1(10, 20); // 构建一个包含10个元素为20的vec
std::vector<int> vec2{10, 20}; // 构建一个包含2个元素为10和20的vec
}
Item 8: Prefer nullptr to 0 and NULL
0
和NULL
在C++98中虽然会用来初始化一个空指针,但准确地来说,它们的类型并不是指针(0
的类型是int
;NULL
的类型取决于具体实现,但必不是指针类型)。
所以在C++11中引入了nullptr
来代表一个空指针。而我们也应该使用nullptr
代替0
和NULL
来代表空指针。
Item 9: Prefer alias declarations to typedefs
C++98中定义类型别名,使用typedef
,例如:typedef std::unique_ptr<std::unordered_map<std::string, std::string>> UPtrMapSS;
C++11中定义类型别名,使用using
,例如:using UPtrMapSS = std::unique_ptr<std::unordered_map<std::string, std::string>>;
使用using
的优势在哪里?
- 定义一个函数指针的别名;明显使用
using
会更直观;
typedef void (*my_func1)(int, const std::string&);
using my_func2 = void (*)(int, const std::string&);
- 对模板取一个别名;
using
可以直接对一个模板取别名;typedef
必须借助定义一个struct
和额外typedef
才能实现取别名的方式。
// 使用using对一个模板取别名
template <typename T>
using MyList = std::list<T>;
MyList<Sample> my_list;
// 使用typedef对一个模板取别名
template <typename T>
struct MyList2 {
typedef std::list<T> type;
};
MyList2<Sample>::type my_list2;
Item 10: Prefer scoped enums to unscoped enums
enum class
是scoped enums
(限域枚举),而enum
是unscoped enums
(未限域枚举)。
接下来介绍这两种枚举类型的区别以及限域枚举的优势。
scoped enums
限域枚举可避免命名空间污染;
C++98的enum
声明的枚举名的名字作用域和该enum
是同一个作用域(这种枚举叫做未限域枚举),这样就需要我们保证enum
定义的枚举名字和作用域内其他变量名字不能冲突。
enum Color { black, white, green };
// 编译器告警,重复定义:Redefinition of 'white' as different kind of symbol
int white = 10;
C++11的enum class
是限域枚举,也就是enum
内部定义的枚举名字作用域在限制在当前enum
中,避免了命名空间污染。
enum class Color { black, white, green };
// 正常编译通过
int white = 10;
scoped enums
限域枚举具有强类型;
未限域enum
中的枚举名会隐式转换为整型;而不存在任何隐式转换可以将限域enum
中的枚举名转化为任何其他类型(需要使用static_cast
);
enum class ScopedColor { black, white, green };
enum UnScopedColor { black, white, green };
int main() {
UnScopedColor w1 = white;
ScopedColor w2 = ScopedColor::white;
if (w1 < 5.5) {
printf("UnScopedColor < 5.5\n");
}
if (static_cast<int>(w2) < 5.5) {
printf("ScopedColor < 5.5\n");
}
}
- 限域
enum
总是可以前置声明;非限域enum
仅当指定它们的底层类型时才能前置;
限域enum
可以被前置声明;非限域enum
不可以被前置声明。
enum class ScopedColor;
// 编译器告警:ISO C++ forbids forward references to 'enum' types
enum UnScopedColor;
这是因为:在C++中所有的enum
都有一个由编译器决定的整型的底层类型来表示该枚举。
所以,C++98只支持enum
定义(所有枚举名全部列出来);enum
声明是不被允许的。需要编译器能在使用之前为每一个enum
选择一个底层类型。
需要注意的是:
为了高效使用内存,编译器通常在确保能包含所有枚举值的前提下为enum选择一个最小的底层类型;
在一些情况下,编译器将会优化速度,舍弃大小,这种情况下它可能不会选择最小的底层类型,而是选择对优化大小有帮助的类型。
那如何声明才能让C++11做到C++98不能做到的事情呢,让非限域enum
前置?即:通过指定enum
的底层类型。
- 默认情况下,限域枚举的底层类型是
int
;如果默认的int
不适用可以重写; - 非限域
enum
也可以指定底层类型;
enum class ScopedColor : uint64_t;
enum UnScopedColor : uint32_t;
enum class ScopedColor : uint64_t { black, white, green };
enum UnScopedColor : uint32_t { good = 0, failed = 1, incomplete = 100, corrupt = 200, indeterminate = 0xFFFFFFFF };
int main() {
printf("%lu\n", sizeof(ScopedColor));
printf("%lu\n", sizeof(UnScopedColor));
}
Item 11: Prefer deleted functions to private undefined ones
在C++98中防止调用类的某些函数,可以将这些函数定义为private
的;在C++11中可以使用= delete
表明该函数不可使用。
deleted
函数具备的优势有以下三点:
- 应用于类内成员函数不可调用;
- 应用于普通函数不可调用;
- 应用于禁止一些模板实例化;
class Sample {
public:
void func(int a, int b) = delete; // 1. 类内成员函数delete
};
void func2(int a);
void func2(const std::string &str) = delete; // 2. 普通函数delete
template <typename T>
void processPointer(T *ptr);
template <>
void processPointer<void>(void *) = delete; // 3. 禁止模板实例化
template <>
void processPointer<char>(char *) = delete; // 3. 禁止模板实例化
Item 12: Declare overriding functions override
重写派生类中与基类同名的函数时,建议后缀添加override
,这样可以体现出该函数是重写了基类函数,并且编译器也可以帮我们进行重写的一些检查。
class Sample {
public:
virtual void func(int a, int b);
virtual ~Sample();
};
class SonOfSample : public Sample {
public:
void func(int a, int b) override;
~SonOfSample();
};
但是,重写一个函数,需要满足以下要求:
- 基类函数必须是
virtual
; - 基类和派生类函数名必须完全一样(除非是析构函数);
- 基类和派生类函数形参类型必须完全一样;
- 基类和派生类函数常量性
const
必须完全一样; - 基类和派生类函数的返回值和异常说明(exception specifications)必须兼容;
- C++11新增:函数的引用限定符(reference qualifiers)必须完全一样;
啥是引用限定符呢?从下面这个代码例子中理解下,
class Widget {
public:
void doWork() &; // 只有*this为左值的时候才能被调用
void doWork() &&; // 只有*this为右值的时候才能被调用
};
Widget makeWidget(); // 工厂函数(返回右值)
Widget w; // 普通对象(左值)
w.doWork(); // 调用被左值引用限定修饰的Widget::doWork版本(即Widget::doWork &)
makeWidget().doWork(); // 调用被右值引用限定修饰的Widget::doWork版本(即Widget::doWork &&)
Item 13: Prefer const_iterators
to iterators
STL中的const_iterator
等价于指向常量的指针pointer-to-const
。它们都指向不能被修改的值。
标准实践是能加上const
就加上,这也指示我们需要一个迭代器时只要没必要修改迭代器指向的值,就应当使用const_iterator
。
std::vector<int> values;
// it -> std::vector<int>::const_iterator
auto it = std::find(values.cbegin(), values.cend(), 1983);
values.insert(it, 1998);
Item 14: Declare functions noexcept
if they won't emit exceptions
在C++11中,无条件的noexcept
保证函数不会抛出任何异常。
- 有异常安全保证:告诉接口使用者该函数不会抛出异常,那使用者就不会再单独编写处理异常的代码了;
- 移动语义:在C++11中,移动构造函数和移动赋值操作通常是不抛异常的,这样标准库容器在重新分配内存时可以安全使用这些操作来转移对象,而非复制对象;
- 性能优化:编译器知道一个函数不会抛出异常,它可以生成更优化的代码。
详细记录下第3点,在C++中,异常处理机制允许程序在遇到错误或者不寻常的情况时,能够抛出异常,并在调用栈的更高层次上捕获并处理这些异常。这是一种强大的错误处理方式,但它也带来了性能上的开销。当一个函数可能抛出异常时,编译器必须能够支持在异常发生时进行适当处理。这通常涉及以下方面:
- 栈展开(Stack Unwinding):当异常被抛出时,程序必须找到一个能够处理该异常的捕获点(catch block)。为了到达这个捕获点,程序可能需要退出当前函数,并且逐层回溯调用栈,直到找到合适的捕获点。在这个过程中,程序需要销毁所有局部对象(调用析构函数来完成)。这个过程称为栈展开,它可能涉及到大量的代码执行,尤其是当调用栈很深时。
- 异常表(Exception Tables):为了支持栈展开,编译器需要生成额外的数据结构,通常是异常表,来记录每个函数中可能抛出异常的位置,以及如何进行栈展开。这些信息需要在运行时可用,这意味着它会增加程序的大小,并且可能影响到缓存和内存使用。
- 当一个函数被标记为
noexcept
时,编译器知道这个函数保证不会抛出异常。这样,编译器就可以对这个函数的实现进行以上两方面的优化。
Item 15: Use constexpr
whenever possible
constexpr
是C++11引入的一个关键字,它用来定义常量表达式。这个关键字的引入主要是为了解决编译时计算的问题,提高程序的性能,并允许更多的编译时类型检查。
在constexpr
出现之前,C++中的常量表达式主要是通过const
关键字或者#define
宏来定义的。但是这两种方式都有局限性:
const
只能保证变量的值不被改变,但并不能保证其值在编译时就已经确定,因此不能用于所有需要编译时确定值的场景。#define
宏没有类型安全,容易引起错误,并且它的作用域是全局的,容易造成命名冲突。
constexpr
可以用于变量、函数和构造函数:
constexpr
变量必须由编译时已知的值初始化,并且其类型必须是字面类型(Literal Type);constexpr
函数可以在编译时对其参数进行计算,前提是所有参数都是编译时常量。这样的函数体内部有一些限制,不能有任何不确定的行为;constexpr
构造函数允许对象在编译时被创建和初始化。这样的对象必须只包含字面类型的成员,并且成员的初始化也必须是编译时常量。
// 编译时常量
constexpr int max_size = 100;
constexpr double gravity = 9.81;
// 编译时函数
constexpr int square(int x) { return x * x; }
constexpr int val = square(5);
// 编译时构造函数
class Point {
public:
constexpr Point(double xVal, double yVal) : x(xVal), y(yVal) {}
constexpr double getX() const { return x; }
constexpr double getY() const { return y; }
private:
double x, y;
};
constexpr Point p(9.4, 27.7);
标签:std,初始化,int,enum,C++,Sample,填坑,EffectiveModern
From: https://www.cnblogs.com/pplearn/p/18048633