C++实战笔记(二)

final标识符

C++11新增final标识符,把final用于类定义,就可以显示地禁用继承,防止有人有意或者无意创建派生类,这个标识符无论对人还是编译器,效果都很好,建议积极使用。例如:

class DemoClass final  //禁止任何人继承
{...}

final也可以用于虚函数,禁止这个虚函数再被子类重载,这样就可以更精细地控制继承类的使用:

class Interface   //接口类定义,没有final,可以被继承
{
    virtual void f() = 0;  //纯虚接口,没有final,可以被子类继承
    ...
};

class Abstract : public Interface  //抽象类,没有final,可以被继承
{
    virtual void f() final   //虚函数,有final,禁止子类重载
    {....}
};

在必须使用继承的场合,建议只使用public,当到达继承体系底层时,及时使用final,终止继承关系。

class Implement final :   //实现类,final禁止其被继承
    public Abstract
{...}

default/delete 函数

对于比较重要的改造函数和析构函数,应该用"=default"的形式明确地告诉编译器(和代码阅读者):“应该实现这个函数,但我不想自己写”。这样编译器就得到了明确的指示,可以更好地进行优化。

class DemoClass final
{
public:
    DemoClass() = default;
    ~DemoClass() = default;  //明确告诉编译器,使用默认实现

    DemoClass(...)  {...};
    DemoClass(...)  {...};  //其他形式的构造函数,不用默认实现
};

default函数只是简化了默认构造函数/析构函数的写法,我们仍然可以写出其他形式的构造/析构函数,所以它们的作用相当于为类提供了一个"保底"实现。default主要用于构造、赋值、析构。

与之相似的,"=delete"它表示明确地禁用某个函数形式,可以用于类内的任何函数。

class DemoClass final
{
public:
    DemoClass(const DemoClass&)  = delete;  //禁止构造函数
    DemoClass& operator=(const DemoClass) = delete;  //禁止赋值函数
};

explicit函数

C++有隐式构造和隐式转型的规则,如果类里有单参数的构造函数,或者是转型操作符函数,为了防止意外的类型转换,保证安全,最好使用explicit将这些函数标记为"显示"。

class DemoClass final
{
public:
    explicit DemoClass(const string_type& str)  //显式单参数构造函数
    { ... }

    explicit operator bool()    //显示转型为bool
    { ... }

};

DemoClass obj = "string ctor";  //错误,不能隐式转换
bool b = obj;                   //错误,不能隐式转换

DemoClass obj = (DemoClass)"string ctor"; //正确,能显式转换
bool b = static_cast<bool>(obj);          //正确的显示转换

if(obj) {...}

委托构造

如果类有多个不同显示的构造函数,为了初始化,成员肯定会有大量的重复代码。为避免重复,常见做法是把公共部分提取出来,放入init()函数里,然后用构造函数去调用,这种方法虽然可行,但效率和可读性差。

现在我们可以使用委托构造的新特性,即在一个构造函数中直接调用另一个构造函数,把构造工作委托出去,既简单又高效

class DemoClass final
{
public:
	DemoClass(int x) : a(x)
	{}

	DemoClass() : DemoClass(0)
	{}

	explicit DemoClass(const std::string& str)
		:DemoClass(stoi(s))
	{}

	explicit operator bool()
	{
		return true;
	}

private:
	int a;   //成员变量
};

成员变量初始化

如果类有很多成员变量,那么在写构造函数时候比较麻烦,不仅不美观,还可能出错。

在现代C++里,我们可以在类里声明变量的同时给它赋值,实现初始化,这样不但清晰,也能消除隐患。

class DemoInit final
{
private:
    int   a = 0;
    string  s = "hello";
    vector<int>  v{1, 2, 3};
public:
    DemoInit() = default;
    ~DemoInit() = default;

public:
    DemoInit(int x) : a(x) {} //也可以单独初始化成员变量,其他未被初始化的成员变量用默认值
};

静态成员变量初始化

对于类的静态成员变量来说,初始化有其特殊性。

如果是const static,C++允许直接在声明的时候初始化,如果是非const的静态成员变量,则要求必须在实现文件里,也就是"*.cpp"里单独再初始化(因为需要分配唯一的存储空间)。

class DemoInit final
{
public:
    static const int  x = 0;
    static string prefix = "xx"; //无法编译
};

string DemoInit::prefix = "xx";  //必须在类外单独初始化

C++17的解决方案,加上一个inline内联关键字就可以,这样被称为内联变量:

class DemoInit final
{
public:
    inline static string prefix = "xx";  //在C++17编译正常
}

使用内联变量,C++就可以保证:无论这个头文件被包含多少次,静态成员变量也只有唯一的一个全局实例。

类型别名

C++扩展了using 的用法,使它具备了typedef的能力,可以定义类型别名。

using uint_t = unsigned int;  //使用using定义别名
typedef unsigned int  uint_t;  //等价的typedef语句

类中使用示例:

class DemoClass final
{
public:
    using this_type = DemoClass;            //给类本身起别名
    using kafka_conf_type = KafkaConfig;   //给外部类型起别名

public:
    using string_type  = std::string;
    using uint32_tupe = uint32_t;

    using set_type = std::set<int>;
    using vector_type = std::vector<std::string>;

private:
    string_type m_name = "tom";
    uint32_type m_age = 23;
    set_type m_books;

private:
    kafka_conf_type m_config;
}

auto关键字

auto的自动推导能力只能使用在初始化的场合,具体来说,就是赋值初始化或者使用花括号初始化,变量右边必须有一个表达式。

auto x = 0L;  //自动推导为long
auto y = &x;  //自动推导为long*
auto z {&x};  //自动推导为long*

auto err;  //错误,没有赋值表达式,不知道什么类型

在类成员变量初始化时候,目前C++标准还不允许使用自动推导类型。

  1. auto总是推导出值类型,绝不会推导出引用类型
  2. auto可以附加const/volatile/*/&等类型修饰符,从而得到新的类型
  3. auto&&是特殊用法,总是推导出引用类型。
  4. auto一般会忽略掉顶层const,同时底层const会保留下来。设置一个类型为auto的引用时,初始值的顶层常量属性仍然保留
auto x = 10L; //auto 推导为long
auto &x1 = x; //auto 推导为long, x1是long&
auto *x2 = &x; //auto 推导为long, x1是long*
const auto &x3 = x; //auto 推导为long, x1是long*
auto x4 = &x3;  //auto推导为const long*,x4是 const long*
auto &&x5 = x;  //auto推导为long,x5是long&

auto就像一个占位符,它推导出的类型会因表达式和附带的修饰符而变化。如果不采用"auto&&"的形式,那么它最终推导出的结果绝对不会是引用,但可以是指针。

decltype关键字

decltype的形式很像函数,后面的圆括号里就是自带的、可用于计算类型的表达式(类似sizeof),其他方面就和auto一样,也能附加const/volatile/*/&来修饰。

因为它已经自带表达式了,所以不需要变量后面再有表达式了,也就是可以直接声明变量,不用赋值、初始化。

int x = 0;
decltype(x)  x1;  //推导为int, x1为int
decltype(x)&  x2 = x;  //推导为int, x2为int&
decltype(x)*  x3; //推导为int, x3为int*
decltype(&x)  x4; //推导为int*,x4为int*
decltype(&x)* x5; //推导为int*,x5为int**

decltype与auto区别:decltype不仅能够推导出值类型,还能够推导出引用类型,也就是表达式的原始类型。

decltype(x2) x6 = x2;  //x2是int&,x6是int&
auto x7 = x2;   //x2是int&,x7是int

我们完全可以把decltype表达式看成一个真正的类型名,用在变量声明、函数参数/返回值、模板参数等任何类型能出现的地方——只不过这个类型是在编译阶段通过表达式"计算"得到的。

using int_ptr = decltype(&x);  //int *
using int_ref = decltype(x)&;  //int &

decltype(auto) c++14新增形式,这里的auto仍然起到了占位符的作用,从而机能精确推导类型,又能像auto一样便于使用:

int x = 0;
decltype(auto) x1 = (x);  //推导为int&,因为(expr)是引用类型
decltype(auto) x2 = &x;   //推导为int*
decltype(auto) x3 = x1;   //推导为int&

使用auto

auto有一个“最佳实践”,就是用于"range-based for"(基于范围的for循环):不需要关心容器的元素类型、迭代器返回值、首末位置,就能非常轻松完成遍历操作。不过为了保证效率,最好使用"const auto&"或者"auto&",例如:

vector<int> v = {2, 5, 7, 9, 11};  //vector顺序容器

for(const auto& i : v){
    cout << i << ","; 
}

for(auto& i : v){
    i++;
    cout << i << ","; 
}

// C++14之后,可以推导函数返回值
auto get_a_set()
{
    std::set<int> s = {1, 2, 3};
    return s;   //返回一个容器对象
}

// C++17,增加结构化绑定功能
tuple x{1, "x"s, 0.1};  //3个元素的元组
auto [a, b, c] = x;//结构化绑定,取出内部的元素
assert(a == 1 && b == "x");

使用decltype

可用于比较复杂的声明

void (*signal(int signo, void (*func)(int)))(int)

可以使用以下方式:

using sig_func_ptr_t = decltype(&signal);

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值