枚举 #
枚举实现了内容与整形相关联的数据结构,是隐式的编译时常量。
枚举类型,枚举符和枚举变量。
枚举符会放入当前命名空间和其本身的命名空间中,如果是全局的枚举(unscoped),那么可以直接使用。
对枚举变量空初始化会使得枚举符是0,所以推荐把第一个枚举符设置成未知/无效。
枚举类用enum class Name{};定义,只能在其本身的作用域使用,无法被隐式转换成整数,在安全性上更优。
可以在需要的使用using enum Name;来简化使用(C++20)。
结构体 #
对其中某些参数进行初始化:
struct Worker {
int id{};
int age{};
double wage{};
};
Worker John { .id{1},.wage{5000}}; //部分列表初始化,age初始化为0,clangd有bug会警告
Worker John { .id{1},John.age, .wage{5000}}; //这样可以占位,一样使得age初始化为0
结构体模板 #
结构体当然也可以传入模板参数,不过在C++20更加完善,如果使用C++17,可能需要手动指定推断:
template <typename T>
struct Pair
{
T first{};
T second{};
};
// Here's a deduction guide for our Pair (needed in C++17 only)
// Pair objects initialized with arguments of type T and T should deduce to Pair<T>
template <typename T>
Pair(T, T) -> Pair<T>;
int main()
{
Pair<int> p1{ 1, 2 }; // explicitly specify class template Pair<int> (C++11 onward)
Pair p2{ 1, 2 }; // CTAD used to deduce Pair<int> from the initializers (C++17)
return 0;
}
可以为类型模板起别名:
template <typename T>
using Coord = Pair<T>; // Coord is an alias for Pair<T>
类 #
类的成员变量,成员函数是隐式内联的,所以放在头文件中被多个源文件引用的时候,不违反ODR原则。
在类中,不强制声明语句位于使用之前,只要他们都在同一个类中即可。
调用常量类对象内部的函数,要将此成员函数声明为const。
void foo() const {
//...
}
struct和class仅有的差异:struct默认使用public:,而class默认使用private:。在继承性上也是这样。
protect:允许派生的类访问,其可见性介于两者之间。
推荐实践 #
- 不要让成员函数返回引用,否则将会使得成员变量被以外修改。
- 不推荐使用成员函数,为了使类的实现更加简洁。
成员初始化列表 #
现代C++使用成员初始化列表,而不是直接初始化列表。具体是:
class Foo{
int m_a{};
int m_b{};
Foo(int a, int b) : m_a{a}, m_b{b} {
//other...
}
}
以前(C++03)之前则是用的()直接初始化。
成员初始化的顺序和成员初始化列表无关,只跟泪中定义成员变量的顺序有关。
std::string_view特殊处理
#
一个例子:
#include <iostream>
#include <string>
#include <string_view>
class Ball
{
private:
std::string m_color { "none" };
double m_radius { 0.0 };
public:
Ball(std::string_view color, double radius)
: m_color { color }
, m_radius { radius }
{
}
const std::string& getColor() const { return m_color; }
double getRadius() const { return m_radius; }
};
void print(const Ball& ball)
{
std::cout << "Ball(" << ball.getColor() << ", " << ball.getRadius() << ")\n";
}
int main()
{
Ball blue { "blue", 10.0 };
print(blue);
Ball red { "red", 12.0 };
print(red);
return 0;
}
为什么这里构造函数需要用
string_view作为参数?不能用const string&来替换?
string_view提供了灵活性,如果选择后者,那么如果传入一个C语言的字面量字符串的时候,就必须在堆上构建临时的string对象,而这对性能开销很大;对于前者,永远不会构造临时对象,而是只有一个指针,在真正初始化的时候完成一次复制。(甚至后面还能用移动语义)
为什么
getter不能像上面一样反过来?
因为在类内部的存储对象就是真正的string,给出它的引用使得外部访问的类型明确,不会造成歧义;同时也提示这个引用的生命周期是明确的————跟成员变量一致。
构造函数 #
构造函数用于初始化类,在此我们可以手动指定成员变量如何初始化。
转化构造函数 #
转化构造函数其实就是特殊的构造函数,其只拥有一个参数。如果传入一个该参数类型的变量,那么就会自动把这这个变量转化成类。
默认构造函数 #
class Foo{
int m_a;
int m_b{};
public:
Foo () = default; //可以显式的声明构造函数为默认构造函数,此时m_a会被默认初始化为0。
Foo () {} //用户自己初始化,但是空初始化,此时m_a不会被初始化,是垃圾值。
}
委托构造函数 #
在一个构造函数的:后面写上参数更多的重载构造函数,可以用另一个构造函数来初始化,但是此构造函数就不能再初始化成员变量。
尽量不要使用太多的构造函数。
复制构造函数 #
复制构造函数的参数必须是引用,要不然就会发生无限递归的调用复制函数。
复制构造函数不应用于复制以外的意图,因为编译器可能会发生复制省略(copy elision)
explicit关键字 #
禁止隐式转换:从单个参数隐式转换成类对象;从列表{xxx,xxx}转换成类对象。
在具有单个参数的构造函数前加入,避免编译器执行隐式转换。
由于C++对于用户定义的转换,只允许转换一次,所以下面的代码会报错:
#include <iostream>
#include <string>
#include <string_view>
class Employee
{
private:
std::string m_name{};
public:
Employee(std::string_view name)
: m_name{ name }
{
}
const std::string& getName() const { return m_name; }
};
void printEmployee(Employee e) // has an Employee parameter
{
std::cout << e.getName();
}
int main()
{
// 此时要经历两次转换:C-Style string -> string_view -> class Employee
printEmployee("Joe"); // compile error
// 修正:
// 方法1:
// using std::literals;
// printEmployee("Joe"sv);
// 方法2:
// printEmployee(Employee{"Joe"});
return 0;
}
不对复制/移动构造函数使用explicit,因为他们不执行隐式转换。
如果转换的时候两者等效且零开销,可以不使用explicit。
比如:
const char*->string_viewstring->string_view
constexpr问题 #
从C++14开始,constexpr修饰函数仅仅是作为编译时求值的提示,如果传入的变量不是一个constexpr,那么这个函数就具有运行时的上下文,constexpr修饰就不起作用。
对于struct,其作为一个聚合体,默认的构造函数无须加入constexpr就可以用它初始化一个类对象。但是对于class,就必须public,同时手动指定构造函数是constexpr,比如:
Foo {
public:
constexpr Foo = default;
}
而constexpr修饰一个类对象的时候,如果涉及到的函数(构造函数、setter以及使用到的成员函数)都有constexpr修饰,表示这个类具有编译时的上下文,会让此对象成为一个编译时常量。在编译时如果此对象是用临时对象初始化的,对临时对象是可以修改的,但是一旦这个对象初始化完毕,那么就不再可以修改。
后续要访问此constexpr对象,那么所有的函数()后,都必须有const,保证不修改此对象。
参考(learncpp - 14.17)[https://www.learncpp.com/cpp-tutorial/constexpr-aggregates-and-classes/]
this指针 #
this指针是一个const指针(顶层),指向当前操作的类。
this指针出现比引用早,不然它多少是个引用。
成员函数类外定义 #
成员函数可以在类外定义,要加上域访问解析符::。如果是类的正下方(.h中),前面加inline关键字以防止重复包含;如果是对应的cpp中,无须加inline。
默认参数在声明时给出。
类型模板参数 #
可以在类指定类型模板参数,不过如果成员函数先声明,然后在类外定义,那么需要单独再指定一次模板参数。
模板类外的成员函数要紧挨着类定义的下面。
注意:
- 所有的模板函数都是默认内联的,所以就算在类外定义,已经隐式
inline了。 - 类外定义的函数的类型模板参数必须与类的一致。
传入类模板的参数,无须加<T>,因为已经在Pair<T>::作用域中了。
template <typename T>
bool Pair<T>::isEqual(const Pair& pair) // note the parameter has type Pair, not Pair<T>
{
return m_first == pair.m_first && m_second == pair.m_second;
}
静态成员变量 #
静态成员变量在所有实例化的对象都可用,具有相同的值,在未实例化的时候也可以使用,直接用类名和域访问解析符访问。
声明的时候,在类内加static关键字,类外定义不能加关键字。
位置:直接在类后面/类对应的cpp,头文件中可以加inline。
只有静态成员变量可以自动推断,普通的不允许。
用途:一个根据数量递增的ID
静态成员函数 #
静态成员函数用于访问静态全局变量。
有替代品:
- 命名空间:没有访问控制
- 静态全局类对象
友元 #
在被访问的类中声明,从而使得外部的类/函数能够访问private和protected的对象。
友元函数 #
在类中声明,自动成为非成员的函数,类外定义。
此项特性对于运算符重载非常有用。
友元类 #
直接在类内定义。