自定义字面量
字面量(literal)指在源代码中写出的固定常量,在c++98中只能是原生类型如
“hello”字符串字面量 类型是cosnt char[6], 1整型字面量类型是int, 0.0 双精度浮点字面量double, 3.14f单精度浮点数字面量 float,123456789ul 无符号长整型字面量 usigned long
c++11引用自定义字面量,使用operator”” 后缀来将用户提供的字面量转换成实际的类型对象,在c++14的标准库中就用这种方法定义了许多标准字面量
#include <chrono>
#include <complex>
#include <iostream>
#include <string>
#include <thread>
using namespace std;
int main()
{
cout << "i * i = " << 1i * 1i // complex 虚数字面量 operator""i 后缀i
<< endl;
cout << "Waiting for 500ms"
<< endl;
this_thread::sleep_for(500ms); //chrono 时间字面量 operator""ms 后缀ms
cout << "Hello world"s.substr(0, 5)// basic_string字面量operator""s 后缀s
<< endl;
}
输出是:i i = (-1,0)
Waiting for 500ms
Hello
上面使用了using namespace std; ,这同时会引入std名空间和里面的内联名空间(inline namespace),包括了上面的字面量运算符所在的三个命名空间
std::literals::complex_literals
std::literals::chrono_literals
std::literals::string_literals
在实际项目中,不会也不应该全局使用using namespace std,这种情况下应当在 *我们使用这些字面量的作用域里 导入需要的名空间,以免发生冲突
在上面这个例子中应该
int main()
{
//我们使用这些字面量的作用域里 导入需要的名空间
using namespace std::literals::complex_literals
using namespace std::literals::chrono_literals
using namespace std::literals::string_literals
cout << "i * i = " << 1i * 1i // complex 虚数字面量 operator""i 后缀i
<< endl;
cout << "Waiting for 500ms"
<< endl;
this_thread::sleep_for(500ms); //chrono 时间字面量 operator""ms 后缀ms
cout << "Hello world"s.substr(0, 5)// basic_string字面量operator""s 后缀s
<< endl;
}
要自己的类支持字面量也很容易,唯一的限制是定义 字面量op时必须以下划线_打头
比如
length operator”” _m(long double v){}
//为一个长度类定义字面量op
struct length {
double value;
enum unit {
metre,
kilometre,
millimetre,
centimetre,
inch,
foot,
yard,
mile,
};
static constexpr double factors[] =
{1.0, 1000.0, 1e-3,
1e-2, 0.0254, 0.3048,
0.9144, 1609.344};
explicit length(double v,
unit u = metre)
{
value = v * factors[u];
}
};
length operator+(length lhs,
length rhs)
{
return length(lhs.value +
rhs.value);
}
// 可能有其他运算符
length operator"" _m(long double v)//米单位字面量op
{
return length(v, length::metre);
}
length operator"" _cm(long double v)//厘米单位字面量op
{
return length(v, length::centimetre);
}
1.0_m + 10.0_cm 有了上面的三个op函数,这个表达式就能正常运行
二进制字面量
c中就有0x前缀,可以让程序员直接写出0xff这样的十六进制字面量,还有就是0后面直接跟0-7数字来表示八进制字面量。
从c++14开始对于二进制也有了直接的字面量,以0b开头
unsigned mask = 0b111000000;
这在需要比特级操作的场合非常有用
不过,遗憾的是, I/O streams 里只有 dec、hex、oct 三个操纵器(manipulator),而没有 bin,因而输出一个二进制数不能像十进制、十六进制、八进制那么直接。一个间接方式是使用 bitset,但调用者需要手工指定二进制位数:
#include <bitset>
cout << bitset<9>(mask) << endl;//手动指定二进制位数
数字分隔符
数字长了之后,看清位数就变得很麻烦特别是二进制字面量,c++14开始允许在数字型字面量中任意添加’来分隔 使得可读性更好
unsigned mask = 0b111'000'000;//3位 二进制 分隔 提高可读性
long r_earth_equatorial = 6'378'137;
double pi = 3.14159'26535'89793;
const unsigned magic = 0x44'42'47'4E;
静态断言
c++98的assert允许在运行时检查一个函数的前置条件是否成立,但是没有一种方法允许在编译时检查假设是否成立。
例如有个模板参数alignment,表示对齐,我们需要再编译时就检查alignment是不是二的整数幂,c++98使用下面的方法来实现
#define STATIC_ASSERT(_Expr,_Msg) static_assert(_Expr,#_Msg)
通过alignment&(alignment-1) =? 0来判断alignment是否为二的整数幂
STATIC_ASSERT( alignment&(alignment-1) ) == 0; //是用static_assert返回是否为0来判断
输出的信息并不是那么直观
在c++11中直接从语言层面提供了静态断言机制,能输出更好的信息,而且适用性也更好,可以直接放在类定义中,而不像之前的技巧只能放在函数体里
语法上就是下面这样
static_assert(编译期条件表达式,可选输出信息);
c++11中就可以像下面这样写
static_assert((alignment & (alignment - 1)) == 0,"Alignment must be power of two");
default和delete成员函数
一个类定义时,c++编译器可能自动生成一些默认的特殊成员函数,这些特殊成员函数包括:
无参默认构造函数
析构函数
拷贝构造(默认数据成员拷贝赋值)
拷贝赋值函数(op=)
移动构造函数
移动赋值函数
是否生成或不生成这些函数的规则比较复杂可以参考 中的特殊成员函数
https://en.cppreference.com/w/cpp/language/member_functions
每个特殊成员函数可以有3种不同状态, 不同状态可以组合,虽然并不是所有的组合都有效
隐式声明还是用户声明(显式定义) —-不是explict,explict是是否可以隐式地调用
默认提供还是用户提供 default
正常状态还是删除状态 delete
一般在成员和父类没有特殊原因导致 此类对象 不可拷贝或移动 的情况下,用户不显式定义这些成员函数的情况下,编译器会自动产生这些成员函数—(隐式声明,默认提供,正常状态)
下面是一些编译器在一些情况下会做的选择
如果类内有未初始化的 非静态const数据成员 或 引用类型数据成员 将导致默认构造函数被删除
类内有 非静态const数据成员 或 引用类型数据成员 将导致默认的拷贝构造、拷贝赋值、移动构造、移动赋值被删除
只要用户提供了任一构造函数,编译器就不会提供默认无参构造
用户没有提供形如Obj(Obj&)或Obj(const Obj&) (不含模板 就是自己类型的拷贝构造) 编译器会隐式声明一个
用户没有提供形如Obj& opeartor=(Obj&)或Obj& opeartor=(const Obj&) (不含模板 就是自己类型的拷贝赋值) 编译器会隐式声明一个
用户如果声明了一个移动构造或移动赋值, 则默认拷贝构造和拷贝赋值 将被删除
用户没有声明自己的拷贝构造和拷贝赋值和移动赋值和析构函数,编译器隐式地声明一个移动构造
用户没有声明自己的拷贝构造和拷贝赋值和移动构造和析构函数,编译器隐式地声明一个移动赋值
对于移动构造和移动赋值而言 只要声明了所有特殊成员函数中的任一个(除了无参构造),编译器就不会默认提供移动函数,为了安全。
这样的规则还用很多,并且比较复杂,还是在项目中体会其中缘由。
我们这儿主要要说的是,我们可以改变缺省行为,在编译器能默认提供特殊成员函数时将其删除,或在编译器不默认提供特殊成员函数时明确声明其需要默认提供(不过,要注意,即使用户要求默认提供,编译器也可能根据其他规则将特殊成员函数标为删除)。
template <typename T>
class my_array {
public:
my_array(size_t size);
…
private:
T* data_{nullptr};
size_t size_{0};
};
//用户提供了带参构造函数,编译器不会提供默认无参构造
//如果我们需要默认构造函数,就需要我们手工提供这个构造
template <typename T>
class my_array {
public:
my_array(size_t size);
my_array()
: data_(nullptr)
, size_(0) {}
…
private:
T* data_{nullptr};
size_t size_{0};
};
//在c++11中可以
template <typename T>
class my_array {
public:
my_array(size_t size);
my_array() = default; // … 强制编译器提供默认构造
private:
T* data_{nullptr};
size_t size_{0};
};
我们在前面很多讲内讲过shape_wrapper的拷贝行为是不安全的(因为定义了移动行为,想一个资源只能被一个对象拥有),所以我们需要禁止编译器提供的拷贝构造和拷贝赋值,可以用=delete
class shape_wrapper {
…
shape_wrapper(
const shape_wrapper&) = delete;
shape_wrapper& operator=(
const shape_wrapper&) = delete;
…
};
注意一下,用户声明成删除也是一种声明,因此根据上面讲的一大串规则,编译器不会提供默认版本的移动构造和移动赋值函数。
override和final成员函数
c++11新引入的说明符,不是关键词,仅在写到函数声明的尾部起作用。这两个说明符可以组合使用,都是加在类成员函数声明的尾部。
override 显式地声明了成员函数是虚函数且覆盖了父类中的该函数。如果一个函数被声明为override但是类中的虚函数表找不到这个函数(这个函数本身不是虚函数,或父类中不存在这个虚函数),编译器报错
override主要有两个作用:1 给开发人员明确的提示,这个函数覆盖了父类成员的虚函数
2 让编译器进行额外的检查,防止程序员由于拼写错误 导致子类的函数和父类的虚函数名称不一样
class A {
public:
virtual void foo();
virtual void bar();
void foobar();
};
class B : public A {
public:
void foo() override; // OK
//void foobar() override;// 非虚函数不能 override
};
class C : public B {
public:
void foo() override; // OK 不override的话,foo就是B中的foo
};
final声明成员函数是一个虚函数,且该虚函数不可被子类的同名函数覆盖。 两点中的任一点不满足编译器报错
fianl写在类名后 声明某个类或结构体 不可被继承。
class A {
public:
virtual void foo();
virtual void bar();
void foobar();
};
class B : public A {
public:
void foo() override; // OK
void bar() override final; // OK
//final override这种声明 合法但不必要。
//void foobar() override;
// 非虚函数不能 override
};
class C final : public B {
public:
void foo() override; // OK
//void bar() override;
// bar在B父类中被声明为 final final函数不可 override
};
class D : public C {
// C类定义时被声明为final
// 错误:final 类不可派生
…
};
补充一下 over load(重载) over write(隐藏) override(覆盖) 的区别
///////////////////////////////////////////////////////////////////
Overload(重载):在C++程序中,可以将语义、功能相似的几个函数用同一个名字表示,但参数或返回值不同(包括类型、顺序不同),即函数重载。
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;参数的个数,类型,顺序不同都符合重载标准
(4)virtual 关键字可有可无。
Override(覆盖):是指子类函数覆盖父类函数—多态行为,特征是:
(1)不同的范围(分别位于子生类与父类);
(2)函数名、参数列表、返回值类型都与父类中函数相同,或者函数是否被const声明也算
(3)父类函数必须有virtual 关键字。
Overwrite(隐藏):是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果子类的函数与父类的函数同名,但是参数不同。此时,不论有无virtual关键字,父类的函数将被隐藏(注意别与重载混淆)。
(2)如果子生类的函数与父类的函数同名,并且参数也相同,但是父类函数没有virtual关键字。此时,父类的函数被隐藏(注意别与覆盖混淆)。
只要同名函数,不管参数列表是否相同,基类函数都会被隐藏(除了覆盖的那种情况)
可以用对象.父类名::同名隐藏函数()这种形式调用父类中被隐藏的函数
///////////////////////////////////////////////////////////////////