首页 > 编程语言 >C++填坑系列——EffectiveModernC++之新手怎么进入现代C++

C++填坑系列——EffectiveModernC++之新手怎么进入现代C++

时间:2024-03-02 14:56:24浏览次数:27  
标签:std 初始化 int enum C++ Sample 填坑 EffectiveModern

新手怎么进入现代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的好处是啥?

  1. 避免变量未初始化:在编写C++代码时,建议在声明和定义变量时赋予初值,以避免程序默认初始化出意料之外的值。如果使用auto定义变量但未赋初值,编译器会发出警告;
int a = 10;
// 编译器告警:Declaration of variable 'b' with deduced type 'auto' requires an initializer
// auto b;
  1. 省略一个冗长的类型:当我们在遍历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);
}
  1. 定义一个lambda表达式:对于一个lambda表达式,我们可能不确定其具体类型,可以使用auto让编译器来为我们自动推导出类型。
auto func1 = [](int a, int b) { return a + b; };
  1. 避免由于不清楚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)来整合所有情景的初始化语法,也就是统一使用花括号(大括号)来进行各类初始化。

那统一初始化有哪些优势呢?

  1. 花括号初始化不允许内置类型的变窄转换;
  2. 花括号表达式对于C++最令人头疼的解析问题有天生的免疫性(C++规定任何可以被解析为一个声明的东西必须被解析为声明);
  3. 需要了解某些场景下花括号初始化的行为(主要是涉及到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

0NULL在C++98中虽然会用来初始化一个空指针,但准确地来说,它们的类型并不是指针(0的类型是intNULL的类型取决于具体实现,但必不是指针类型)。

所以在C++11中引入了nullptr来代表一个空指针。而我们也应该使用nullptr代替0NULL来代表空指针。

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的优势在哪里?

  1. 定义一个函数指针的别名;明显使用using会更直观;
typedef void (*my_func1)(int, const std::string&);
using my_func2 = void (*)(int, const std::string&);
  1. 对模板取一个别名;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 classscoped enums(限域枚举),而enumunscoped enums(未限域枚举)。

接下来介绍这两种枚举类型的区别以及限域枚举的优势。

  1. 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;
  1. 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");
  }
}
  1. 限域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函数具备的优势有以下三点:

  1. 应用于类内成员函数不可调用;
  2. 应用于普通函数不可调用;
  3. 应用于禁止一些模板实例化;
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保证函数不会抛出任何异常。

  1. 有异常安全保证:告诉接口使用者该函数不会抛出异常,那使用者就不会再单独编写处理异常的代码了;
  2. 移动语义:在C++11中,移动构造函数和移动赋值操作通常是不抛异常的,这样标准库容器在重新分配内存时可以安全使用这些操作来转移对象,而非复制对象;
  3. 性能优化:编译器知道一个函数不会抛出异常,它可以生成更优化的代码。

详细记录下第3点,在C++中,异常处理机制允许程序在遇到错误或者不寻常的情况时,能够抛出异常,并在调用栈的更高层次上捕获并处理这些异常。这是一种强大的错误处理方式,但它也带来了性能上的开销。当一个函数可能抛出异常时,编译器必须能够支持在异常发生时进行适当处理。这通常涉及以下方面:

  1. 栈展开(Stack Unwinding):当异常被抛出时,程序必须找到一个能够处理该异常的捕获点(catch block)。为了到达这个捕获点,程序可能需要退出当前函数,并且逐层回溯调用栈,直到找到合适的捕获点。在这个过程中,程序需要销毁所有局部对象(调用析构函数来完成)。这个过程称为栈展开,它可能涉及到大量的代码执行,尤其是当调用栈很深时。
  2. 异常表(Exception Tables):为了支持栈展开,编译器需要生成额外的数据结构,通常是异常表,来记录每个函数中可能抛出异常的位置,以及如何进行栈展开。这些信息需要在运行时可用,这意味着它会增加程序的大小,并且可能影响到缓存和内存使用。
  3. 当一个函数被标记为noexcept时,编译器知道这个函数保证不会抛出异常。这样,编译器就可以对这个函数的实现进行以上两方面的优化。

Item 15: Use constexpr whenever possible

constexpr是C++11引入的一个关键字,它用来定义常量表达式。这个关键字的引入主要是为了解决编译时计算的问题,提高程序的性能,并允许更多的编译时类型检查。

constexpr出现之前,C++中的常量表达式主要是通过const关键字或者#define宏来定义的。但是这两种方式都有局限性:

  • const 只能保证变量的值不被改变,但并不能保证其值在编译时就已经确定,因此不能用于所有需要编译时确定值的场景。
  • #define 宏没有类型安全,容易引起错误,并且它的作用域是全局的,容易造成命名冲突。

constexpr 可以用于变量、函数和构造函数:

  1. constexpr 变量必须由编译时已知的值初始化,并且其类型必须是字面类型(Literal Type);
  2. constexpr 函数可以在编译时对其参数进行计算,前提是所有参数都是编译时常量。这样的函数体内部有一些限制,不能有任何不确定的行为;
  3. 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

相关文章

  • C++填坑系列——EffectiveModernC++之特殊成员函数
    Chapter移步现代c++之特殊成员函数Item17:Understandspecialmemberfunctiongeneration总结:有必要了解各个函数什么时候自动生成;自动生成的函数有可能产生预期外的行为;特殊成员函数(编译器自动生成):默认构造函数,析构函数,拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋......
  • C++ 多线程笔记2 线程同步
    C++多线程笔记2线程同步并发(Concurrency)和并行(Parallelism)并发是指在单核CPU上,通过时间片轮转的方式,让多个任务看起来像是同时进行的。实际上,CPU在一个时间段内只会处理一个任务,但是由于切换时间非常快,用户感觉像是多个任务同时在进行。这种方式的优点是可以充分利用CPU资源,......
  • C++ 类构造函数 & 析构函数
    带参数的构造函数默认的构造函数没有任何参数,但如果需要,构造函数也可以带有参数。这样在创建对象时就会给对象赋初始值,如下面的例子所示:1#include<iostream>2usingnamespacestd;34classLine5{6public:7voidsetLength(doublelen);8......
  • 万物容器与 c++ 类型反射
    这是一篇组会分享,并且是拖了很长很长时间的那种。这次不会再鸽了这篇文章可以说是针对某cpp佬的公众号的两篇原创内容的笔记c++反射--包容一切的all容器(上)c++反射--包容一切的all容器(中)什么是反射这个好像没有严格的定义,但是概括的说,「反射」是指在程序运行期对程序......
  • C++中cin的详细用法
    1.cin简介cin是C++编程语言中的标准输入流对象,即istream类的对象。cin主要用于从标准输入读取数据,这里的标准输入,指的是终端的键盘。此外,cout是流的对象,即ostream类的对象,cerr是标准错误输出流的对象,也是ostream类的对象。这里的标准输出指的是终端键盘,标准错误输出指的是终端的......
  • C++填坑系列——类型推导 decltype
    decltypedecltype主要是为了解决类型推导的问题,特别是在模板编程和泛型编程中应用较广泛。decltype关键字用于以表达式为参数,推导表达式返回的类型,该类型会保留所有信息。c++11提出的新特性,decltype关键字。和auto一样都是用来做编译时类型推导的,但是也有一些区别:auto:从......
  • C++填坑系列——左值和右值
    c++的表达式首先介绍下c++的表达式是什么?看下cppreference是怎么说的。Anexpressionisasequenceofoperatorsandtheiroperands,thatspecifiesacomputation.也就是说,在C++中,表达式(Expression)是由操作数(Operands)和运算符(Operators)组成的序列。左值和右值就是c++中......
  • C++填坑系列——lambda表达式
    lambda表达式总结:lambda表达式原理:被编译器转换为类+初始化对象的代码;格式:[captureslist](paramslist)specifiersexception->retType{funtionbody}按值捕获和按引用捕获的优缺点以及解决方法;一.lambda原理lambda函数会被编译器转换为类,并定义实现一个operato......
  • C++ 类访问修饰符
    私有(private)成员成员和类的默认访问修饰符是private,如果没有使用任何访问修饰符,类的成员将被假定为私有成员。私有成员变量或函数在类的外部是不可访问的,甚至是不可查看的。只有类和友元函数可以访问私有成员。实际操作中,我们一般会在私有区域定义数据,在公有区域定义相关的函数......
  • C++类开发的第六篇(虚拟继承实现原理和cl命令的使用的bug修复)
    Class_memory接上一篇末尾虚拟继承的简单介绍之后,这篇来详细讲一下这个内存大小是怎么分配的。使用clcl是MicrosoftVisualStudio中的C/C++编译器命令。通过在命令行中键入cl命令,可以调用VisualStudio的编译器进行编译操作。cl命令提供了各种选项和参数,用于指定源......