C++标准特性浅析
- 关于花括号的构造问题
- 隐式转换
- 仿函数与构造函数
- 仿函数的优点是什么
- 为什么static成员函数与虚函数不能一起使用
- 有了C++14更新后的lambda表达式,std::bind还有必要使用吗
- 特种成员函数
- std::ref到底能解决什么问题
- 函数模板
- 现有i++还是先有++i
- 数组能正确处理多态吗
- 为什么std::size_t重要
- 模板的分文件编写
- 返回值为auto和decltype(auto)的函数有什么区别?
- 如何防止模板的重复实例化导致编译时间的增加
- std::bind和std::men_fn的区别是什么
- 为什么拷贝构造函数必须用引用传递
- 构造析构函数抛出异常
- 类如何实现只能静态分配和只能动态分配
- 运算符重载可以是虚函数吗
- 可重载的运算符分哪几类
- 函数调用时调用栈里有什么数据
- 一句话系列
关于花括号的构造问题
class Base {};
Base b1 = {}; // 隐式构造
Base b2{}; // 显式构造
Base b3(); // 声明函数
隐式转换
第一行代码显式构造
第二行代码用到了两个隐式转换,先转换成double
,再转换成Fraction
第三行代码用到了一个隐式转换,转换成Fraction
仿函数与构造函数
class TestClass
{
public:
void operator()() { std::cout << "operator () called" << std::endl; }
};
// 调用一次默认构造函数构建一个对象 因为重载了operator() 所以可以赋值给function对象
std::function<void()> funObj = TestClass();
// 创建匿名变量调用仿函数
TestClass()();
// 创建对象后调用
TestClass t;
t();
仿函数的优点是什么
为什么static成员函数与虚函数不能一起使用
因为虚函数是需要通过对象来调用的,对象需要有this指针,是运行期多态。而静态成员函数是类共有的,是类的属性,因此没有this指针,也无法通过this指针调用到虚表指针再调用到虚函数。
That would make no sense. The point of virtual member functions is that they are dispatched based on the dynamic type of the object instance on which they are called. On the other hand, static functions are not related to any instances and are rather a property of the class. Thus it makes no sense for them to be virtual. If you must, you can use a non-static dispatcher.
有了C++14更新后的lambda表达式,std::bind还有必要使用吗
github-prefer-lambdas-over-std::bind
C++14中完善了lambda表达式,其中包括泛型的lambda表达式(也就是说C++14中lambda表达式能处理静态多态了),而其中又包含move-capture
考虑在C++11中以下代码,Any的实现可以参考上文的知乎链接,以后有时间会分析一下
#include <functional>
#include <iostream>
#include <vector>
struct PollyTypePush
{
template<typename T>
void operator()(std::vector<Any>& container, T&& value) const
{
container.emplace_back(std::forward<T>(value));
}
};
int main()
{
std::vector<Any> container;
auto PollyTypePushFunc = std::bind(PollyTypePush(), std::ref(container), std::placeholders::_1);
PollyTypePushFunc("String");
PollyTypePushFunc(3.14f);
// codes with using container...
return 0;
}
以上代码有两个需要注意的点
- 对于泛型类型的函数,如果想使用
std::bind
对其绑定,而且还想在调用时使用模板函数的自动推导,那么只能使用仿函数 - C++11中的lambda表达式不支持静态多态(即带auto的类型推导),更不支持通用引用
如果使用普通函数或成员函数或静态成员函数来实现emplace_back
的功能,那么只能再进行一次费劲的包装,否则需要在std::bind
时就传入泛型参数
struct PollyTypePush
{
// static成员函数不能声明为常函数
template<typename T>
static void Func(std::vector<Any>& container, T&& value)
{
container.emplace_back(std::forward<T>(value));
}
};
template<typename T>
void AnyContainerPushBack(std::vector<Any>& container, T&& value)
{
container.emplace_back(std::forward<T>(value));
}
int main()
{
std::vector<Any> container;
// 因为是static成员函数 所以可以隐式转换为函数指针 不需要显式取址
auto PollyTypePushFunc1 = std::bind(PollyTypePush::template Func<int>, std::ref(container), std::placeholders::_1);
// 普通函数也需要指定泛化类型
auto PollyTypePushFunc2 = std::bind(AnyContainerPushBack<int>, std::ref(container), std::placeholders::_1);
PollyTypePushFunc1(123);
// 因为是int类型的绑定 所以无法传入const char*的数据
// PollyTypePushFunc2("Mike");
return 0;
}
现在,全体目光向我看齐,我来介绍C++14的lambda写法
#include <functional>
#include <iostream>
#include <vector>
int main()
{
std::vector<Any> container;
auto PollyTypePushFunc = [&container](auto&& param) {
container.template emplace_back(std::forward<decltype(param)>(param));
};
PollyTypePushFunc("String");
PollyTypePushFunc(3.14f);
// codes with using container...
return 0;
}
再介绍一点,std::bind
中的“参数”在绑定时就已经固定了
void print_time(std::chrono::steady_clock::time_point point)
{
using namespace std::chrono;
std::cout << duration_cast<duration<double>>(steady_clock::now() - point).count() << endl;
}
int main()
{
auto func = std::bind(print_time, std::chrono::steady_clock::now());
func(); // 输出近似于0的数
this_thread::sleep_for(chrono::seconds(3));
func(); // 输出近似于3的数
return 0;
}
// 不论什么时候调用都会得到一个接近于0的输出
auto func = []() { print_time(std::chrono::steady_clock::now()); };
// 与上文中std::bind得到的结果几乎一致
auto func = [capturedTime = std::chrono::steady_clock::now()] { print_time(capturedTime); };
特种成员函数
特种成员函数是指C++会自行生成的成员函数。C++11及以后,特种成员函数包括下面六大函数
-
无参构造函数:仅当一个类没有声明任何构造函数的时候,无参构造函数才会被生成
-
析构函数
-
拷贝构造函数
-
拷贝赋值函数
-
移动构造函数
-
移动赋值函数
两种拷贝操作是独立存在的,声明了其中的一个,并不会阻止编译器生成另外一个
两种复制操作并不彼此独立,声明了其中一个,会阻止编译器去生成另外一个
一旦声明了复制操作,编译器就不会生成移动操作了
一旦声明了移动操作,编译器会将复制操作视为delete
析构函数的声明会阻止编译器生成移动操作,而复制操作不会受到影响
std::ref到底能解决什么问题
众所周知,std::ref
经常与std::bind
搭配使用,来确保传递的是引用类型而非是std::bind
默认的值传递。现在来说点别的
请问下列中会调用哪个test_pass
class TestClass
{
public:
TestClass() { cout << "create" << endl; }
};
void test_pass(TestClass t)
{
cout << "value" << endl;
}
void test_pass(const TestClass& t)
{
cout << "reference" << endl;
}
int main()
{
TestClass testClass;
test_pass(std::ref(testClass));
return 0;
}
答案是都不调用,因为根本无法通过编译,两个函数的函数签名是一致的,编译器无法区分
回到正题 问下方代码会输出什么
class TestClass
{
public:
TestClass() { cout << "create" << endl; }
TestClass(const TestClass& t) { cout << "copy" << endl; }
};
void test_pass(const TestClass& t)
{
cout << "reference" << endl;
}
int main()
{
TestClass testClass;
test_pass(testClass);
std::thread newThread(test_pass, testClass);
if (newThread.joinable())
newThread.join();
return 0;
}
答案是线程参数传参的时候进行了一次拷贝构造。如果后续我们在线程中对testClass
执行了各种数据改变操作,那么所有的改变都会应用到拷贝出来的替罪羊身上,而不是testClass
本身,这不会是我们想要的结果
正确的做法是
// 在本例中使用std::ref() 或 std::cref()都可以
std::thread newThread(test_pass, std::cref(testClass));
疑问:std::thread
的初始化不支持重载过的函数吗?
大括号初始化
优点
大括号初始化碰到精度降低,范围变窄等情况(narrowing conversion)时,编译器会给出报错
double num = 3.1415926;
int test1(num); // 编译器给出警告,该转换可能会丢失数据
int test2{num}; // 编译器给出报错,从double转换到int需要收缩转换
同时,支持使用者在自定义类的初始化时使用列表初始化
class Test
{
public:
Test(const std::initializer_list<int>& i_list)
{
vec.reverse(i_list.size());
for (auto i : i_list)
vec.push_back(i);
}
private:
std::vector<int> vec;
};
int main()
{
Test t1{ 1, 2, 3 };
Test t2 = { 1, 2, 3, 4 };
}
缺点
更重要的是,大括号初始化永远会优先匹配std::initializer_list<T>
的版本,当寻找不到std::initializer_list<T>
的重载或T
的类型无法发生转换时,编译器才会去寻找其他重载版本的函数
// 假设Widget的构造函数如下
Widget(int i, bool b);
Widget(int i, double d);
Widget(std::initializer_list<bool> il);
// 创建一个w 编译出错
Widget w{10, 5.0};
正如上文中所说,大括号初始化会优先匹配std::initializer_list<T>
的版本,但是由于10
和5.0
向bool
类型的转换是精度降低的转换(能强转,但是会丢失精度),因此编译会失败。但是如果是下文中的版本
// 假设Widget的构造函数如下
Widget(int i, bool b);
Widget(int i, double d);
Widget(std::initializer_list<std::string> il);
// 创建一个w
Widget w{10, 5.0};
那么编译成功,创建调用到int, double
的构造函数版本。这是因为10
和5.0
本身不存在到std::string
类型的转换,因此编译器会寻找其他版本的重载函数
另外一个例子是std::vector<T>,使用小括号初始化和大括号初始化会产生两种完全不同的结果
// 创建一个包含10个元素的动态数组, 其中元素的值都是20
std::vector vec1(10, 20);
// 创建一个包含两个元素的动态数组, 它们的值分别是10和20
std::vector vec2{10, 20};
大括号初始物的作用是什么
第一是统一,第二是方便
统一
统一了类(结构体),数组(包含STL中的容器,虽然说它们也是类),POD三者的初始化方式。对于前两者来说,只需要在类内部实现一个参数为std::initializer_list
的构造函数即可
class MyClass {
public:
MyClass(const std::initializer_list<int>& iList) {}
};
// 以下三种都是隐式构造 只调用一次构造函数
MyClass m2 = { 1, 2, 3 };
std::map<int, std::string> m = { {1, "11"}, {2, "22"} };
std::vector<int> v1 = { 1, 2, 3, 4 };
std::vector<int> v2{ 1, 2 };
int arr[] = { 1, 2, 3, 4, 5 }; // 可以不指明大小
对于类(结构体)和POD类型
class A_Class
{
private:
int data;
bool isDone;
public:
A_Class(int _data, bool _isDone) : data(_data), isDone(_isDone) {}
};
struct B_POD
{
int data;
bool isDone;
};
A_Class a{ 1, false };
B_POD b{ 2, true };
// 没有提供有参构造函数 错误
// B_POD b(2, true);
POD:Plain Old Data,指没有构造函数,析构函数和虚函数等的类或结构体
- 拥有平凡的默认构造函数(trivial constructor)和析构函数(trivial destructor)
- 拥有平凡的复制构造函数(trivial copy constructor)和移动构造函数(trivial move constructor)
- 拥有平凡的复制赋值运算符(trivial assignment operator)和移动赋值运算符(trivial move operator)
- 不能包含虚拟函数和虚拟基类
方便
对于容器,数组类型,可以直接用初始化列表进行初始化,十分方便
函数模板
C++98和C++11中函数模板的区别
如果泛型函数有参数,那么它可以通过参数来推断出模板类型,不需要显式指定。(显式指定优先级大于参数)
函数模板只有全特化,没有偏特化
如何实现函数模板的偏特化,相关连接:https://zhuanlan.zhihu.com/p/268600376
现有i++还是先有++i
// ++i 前置++
ListNodeIterator& operator++()
{
node = node->nextNode;
return *this;
}
// i++ 后置++
ListNodeIterator operator++(int)
{
// 调用到拷贝构造函数 进行一次默认的浅拷贝 故需要自行书写拷贝构造函数 同时该拷贝构造函数不能为explicit
ListNodeIterator temp = *this;
// ListNodeIterator temp(*this); // 两种写法
operator++();
// ++(*this); // 两种写法
return temp;
}
从这种写法来看的话应该是先有++i
,并且++i
的开销更小,不需要构建一个临时变量。所以在for
循环里头一般写++i
数组能正确处理多态吗
struct Father
{
int fatherData = 10;
virtual ~Father() { std::cout << "Father" << std::endl; }
};
struct Son : public Father
{
long long sonData = 100;
virtual ~Son() { std::cout << "Son" << std::endl; }
};
int main()
{
Father* p = new Son[3];
std::cout << p[0].fatherData << std::endl;
std::cout << p[1].fatherData << std::endl;
std::cout << p[2].fatherData << std::endl;
delete[] p;
}
在32位系统下,内存中的布局大概是这样
f0 9d ea 00 | 0a 00 00 00 | 64 00 00 00 00 00 00 00
f0 9d ea 00 | 0a 00 00 00 | 64 00 00 00 00 00 00 00
f0 9d ea 00 | 0a 00 00 00 | 64 00 00 00 00 00 00 00
可以直观的看到,首位的存放的是虚表指针,因为是32位系统所以占4B。第二个位置代表了fatherData
,数据为10;第三个位置代表了sonData
,数据为100。而在示例代码中我们使用了基类指针指向一个派生类数组,也就代表了我们将以基类指针去解析地址中的数据,因此解析的数据看起来会像是这样
f0 9d ea 00 | 0a 00 00 00
64 00 00 00 | 00 00 00 00
f0 9d ea 00 | 0a 00 00 00
...
所以代码会输出10,0,10,同时若此时通过虚表指针访问函数那么将出错
最后再来看delete[]
,在部分编译器中,会对此操作进行优化,使内存能正常回收。因此最后的结果可能是输出三次Son,Father;也有可能会崩溃
https://stackoverflow.com/questions/216259/is-there-a-max-array-length-limit-in-c)
为什么std::size_t重要
// vcruntime.h
// Definitions of common types
#ifdef _WIN64
typedef unsigned __int64 size_t;
typedef __int64 ptrdiff_t;
typedef __int64 intptr_t;
#else
typedef unsigned int size_t;
typedef int ptrdiff_t;
typedef int intptr_t;
#endif
在MSVS中,64位编译环境std::size_t
为uint64_t
,32位编译环境std::size_t为uint32_t
。而不管在哪个平台上int
的大小一般是4B,与long
一致
long的要求是容量大于等于int,但是在一般情况下long的大小和int相等
sizeof
方法返回的类型就是std::sizeof
,这意味着C++中所有对象的大小都将由std::size_t
这个类型来表示。也就是说所有对象大小的最大值不能够超过std::size_t
能表示的最大值
std::size_t size = size_of(T);
在c++中,你能申请的数组的最大容量约为2GB
// 数组的总大小不得超过 0x7fffffff 字节
// char占1B 数组的大小 = 1 * (2^31 - 1)B = 7fffffffB
char* p = new char[INT32_MAX];
当使用到数组,分配内存空间,表示大小的时候,使用
std::size_t
模板的分文件编写
返回值为auto和decltype(auto)的函数有什么区别?
不单单是返回值的问题,首先应该理解auto
可能会出现推导错误的情况(即丢失引用等信息),假设我们有以下三个方法
string name = "Mike";
string get_name() { return "Jelly"; }
string& get_name_reference() { return name; }
const string& get_name_const_reference() { return name; }
然后我们对其进行封装,然后检测auto推断的类型
auto package_get_name_reference() { return get_name_reference(); }
auto package_get_name_const_reference(){ return get_name_const_reference(); }
int main()
{
cout << boolalpha << is_same<decltype(package_get_name_reference()), string>::value << endl; // true
cout << boolalpha << is_same<decltype(package_get_name_const_reference()), string>::value << endl; // true
}
是的,单纯使用一个auto
,引用或者const
都被扔掉了,那应该怎么办呢
- 对于我们自己知道它是什么类型的,可以加上
&
来“帮助”auto
,让它推断出正确的类型 - 如果说想偷懒或者说无法知道返回值是什么类型的,可以使用
decltype(auto)
auto& package_get_name_reference() { return get_name_reference(); } // 推断为string&
auto& package_get_name_const_reference(){ return get_name_const_reference(); } // 推断为const string&
或者
decltype(auto) package_get_name_reference() { return get_name_reference(); } // 推断为string&
decltype(auto) package_get_name_const_reference(){ return get_name_const_reference(); } // 推断为const string&
在现代C++书籍中也有提到,当碰到复杂的模板等情况的时候,选择使用C++14新加入的decltype(auto)
会更加方便
- 转发函数
- 封装的返回类型
- 复杂的模板
如何防止模板的重复实例化导致编译时间的增加
什么是模板的实例化
假设我们有一个hpp文件,里头定义并实现了一个泛型函数
// TestTemplate.hpp
template<typename T>
void TestExternTemplate(T data)
{
cout << typeid(T).name() << endl;
}
此时有一个cpp文件调用这个泛型函数
// Test1.cpp
#include "TestTemplate.hpp"
void Func1()
{
TestExternTemplate(10);
}
当单独编译这个文件的时候,编译器会在Test1.object中生成一个TestExternTemplate<int>
的实例化
如果此时有一份别的cpp文件也要调用这个泛型函数
// Test2.cpp
#include "TestTemplate.hpp"
void Func2()
{
TestExternTemplate(1000);
}
那么编译之后在Test1.object和Test2.object中会有两份一摸一样的TestExternTemplate<int>
的实例化代码。
虽然编译器可能会进行剔除操作来防止代码的重复,但是这仍然增加了编译和链接的时间
使用外部模板来防止重复实例化
使用extern来“查找”外部模板
// Test1.cpp
#include "TestTemplate.hpp"
template void TestExternTemplate(int);
void Func1()
{
TestExternTemplate(10);
}
// Test2.cpp
#include "TestTemplate.hpp"
extern template void TestExternTemplate<int>(int);
void Func2()
{
TestExternTemplate(1000);
}
std::bind和std::men_fn的区别是什么
std::men_fn
绑定是成员函数,并且不能提前绑定函数参数,也就说明了调用返回的对象时,std::men_fn
不需要使用std::reference_wrapper
(std::bind
在提前绑定引用参数时,需要使用std::reference_wrapper
包装)
struct Foo
{
void greeting() {
std::cout << "Hello world" << std::endl;
}
void show(int i) {
std::cout << "number: " << i << std::endl;
}
int data = 7;
};
int main() {
Foo f;
auto greet = std::mem_fn(&Foo::greeting);
greet(f);
auto print_num = std::mem_fn(&Foo::show);
print_num(f, 42);
auto access_data = std::mem_fn(&Foo::data);
std::cout << "data: " << access_data(f) << std::endl;
auto bindGreet = std::bind(&Foo::greeting, &f);
bindGreet();
auto bindData = std::bind(&Foo::data, &f);
std::cout << "data: " << bindData() << std::endl;
}
为什么拷贝构造函数必须用引用传递
因为值传递会调用到拷贝构造函数,而如果拷贝构造函数也使用值传递的话,那么将会陷入无限的循环中
而拷贝赋值函数最好也使用引用传递,使用值传递会导致在传参的时候产生不必要的拷贝
构造析构函数抛出异常
由于在构造函数中抛出了异常,因此TestClass并没有完整的执行完构造函数,因此t
是不完整的,C++不会对一个不完整的数据执行析构函数。因此t
的析构函数不会被调用,因此可能会导致内存泄漏
class TestClass
{
public:
TestClass() { throw 10; }
~TestClass() { std::cout << "~TestClass" << std::endl; }
};
int main()
{
try {
TestClass t;
}
catch (...) {}
}
析构函数抛出异常也是同理,如果异常没有被处理那么程序将会被中止。正确的做法应该是在析构函数内部处理完异常,不要让他离开析构函数的作用域
类如何实现只能静态分配和只能动态分配
实现只能静态分配
静态分配,也就是说编译器在栈上为类分配内存,这个时候只需要将operator new
和operator delete
设置为private
属性,防止外界调用即可
实现只能动态分配
只能动态分配,意味着不能直接调用类的构造函数。把类的构造函数,析构函数设置为protected
即可,然后利用“工厂”去创建
运算符重载可以是虚函数吗
运算符重载可以是inline
的,也可以是virtual
,但不能同时满足两者。以operator=
来说,可以,但是使用时需要注意
class Father
{
public:
virtual Father& operator=(const Father& father) {
return *this;
}
};
class Son : public Father
{
public:
Son& operator=(const Son& son) override {
return *this;
}
};
以上代码其实是无法通过编译的,因为对于Son::operator=
来说,由于函数签名不一致,因此根本不算是重写基类中的虚函数。如果一定要将operator重载的话,可以这么做
class Father
{
public:
virtual Father& operator=(const Father& father) {
return *this;
}
};
class Son : public Father
{
private:
Father& operator=(const Father& father) override {
return operator=(dynamic_cast<const Son&>(father));
}
public:
Son& operator=(const Son& son) {
return *this;
}
};
可重载的运算符分哪几类
与常理认知不同,在有了operator<
和operator==
之后,如果我们想要实现<=
和!=
,仍然需要显示给出operator<=
和operator!=
的重载,编译器不会自动把!=
转换为!(==)
或执行其他操作
函数调用时调用栈里有什么数据
一句话系列
不允许auto推断数组类型
以下操作是错误的,编译不通过
auto arr[3] = {1, 2, 3};
委托构造
使用委托构造时不能再使用初始化列表初始化类成员
// 非法
Base(int _data) : data(_data), Base() {}
无参构造调用有参构造也是合法的
继承构造
当父类子类存在继承关系时,如果子类不显示提供任何构造函数,那么会默认继承并调用父类的无参构造函数
父类的有参构造等特殊函数并不会被子类隐式继承,而在子类中重写一遍并将参数一一传递的效率是很低下的,所以需要使用到继承构造
// Codes
class SubClass : public Base
{
public:
// 继承使用父类所有的构造函数
using Base::Base;
};
编译器不再为使用继承构造的类创建默认函数
子类调用父类的成员函数?
当函数没有重载时,直接调用即可
当函数重载时,需要加入::
限定词来调用父类中的成员函数
当函数重写时,如何在外部通过子类调用父类的函数
引用能实现多态吗
可以,和指针一样