踩坑日志(二)
目录
文章目录
- 踩坑日志(二)
- 目录
- C++部分
- (1) C++11新关键字decltype的使用方法
- (2) 函数指针
- (3) C++11:using 关键字与模板参数
- (4) C++11: 可变模板参数
- (5) 模板格式的声明
- (6) 移除引用:将一个类型的左值或右值引用转化成该类型的左值(remove_reference)
- (7) 特殊的指针类型:void*
- (8) 使用宏来替换代码块
- (9) Lambda表达式的使用说明
- (10) C++多继承相关问题
- (11) C++11对{}运算符的扩展
- (12) C++11的range-base for syntax 基于范围的for循环语法
- (13) C++函数与数组传参
- (14) 将数组作为模板参数
- (15) 关于运算符new和delete的重载
- 对于这两个运算符重载的风险程度较高,如果疏忽可能会导致一种更为严重的内存泄露——幽灵指针
- Qt部分
C++部分
以下有关编译器的特性全部基于GCC 11.2.0 x86_64-w64-mingw32
(1) C++11新关键字decltype的使用方法
-
decltype是C++11标准新推出的关键字,可以用来取代auto关键字进行类型类型推导,decltype可以更具具体的变量在编译过程中进行类型推导
-
// decltype(exp) 其中exp可以是任意类型的变量(指针,引用(左值引用和右值引用都可以),普通的实例或变量) int a=10; int& b = a; int&& c = 10; decltype(a) // 等价于int decltype(b) // 等价于int& decltype(c) // 等价于int&& // 在某些情况下decltype可以完成auto不能完成的任务,如下面提到的我们想定义一个函数返回值的类型,却不希望执行函数调用, 此时引入新的关键字decltype来解决。
-
decltype可以获取一个函数的返回值类型,并且不执行该函数
-
// 严格来说获取的是函数返回值的右值,不管左值还是右值,本质都是值,要跟引用区分出来 // int func() decltype(func()) // type: int
-
可以增强泛型(模板),根据实际的返回值来声明函数的返回值(decltype在模板中的应用–函数返回值类型后置声明)
- decltype - 知乎 (zhihu.com)
- 参考C++Primer Plus (Stephen Prata) 第六版中文版
-
// 实现一个模板函数,能够完成任意类型的变量的加法运算(c++14) template<typename T1,typename T2> decltype(auto) add(const T1& t1,const T2& t2); // c++11的写法(c++11的返回类型后置) template<typename T1,typename T2> auto add(const T1& t1,const T2& t2)->decltype(T* U);
-
对于普通函数func来说decltype(func)获取的是函数func的函数类型而不是函数指针,可以说函数名并非函数指针
-
void func(){ cout<<"hello world!"<<endl; } decltype(func) ptr = func; /* 此时decltype(func) 是函数类型( void()() )而不是函数指针类型( void(*)() ), 函数类型的变量不可以用函数名来赋值,报错:function 'void ptr()' is initialized like a variable */ decltype(func)* ptr1 = func; // 正确 decltype(func)* ptr2 = &func; // 正确 decltype(&func) ptr3 = func; // 正确 decltype(&func) ptr4 = &func; // 正确 // 下面我们会接着讨论函数名的类型到底是什么。
-
对于非静态成员函数来说,必须通过以下方式声明函数指针
-
// 现在有一个类 class A{ public: void out(){ cout<<"hello"<<endl; } } // 声明函数指针 A a; decltype(A::out) ptr = a.out; /* A::out是一个特殊标识(如果它表示成员函数out的函数名,理应发生如上面普通函数 第4行一样的报错),它不是静态成员函数的函数名,它的类型是out函数的函数指针类型( void(A::*)() ), 而不是out函数的函数类型( void(A::)() )因此也不许添加解引用运算符*了。 不过指的注意的是,有一些IDE会报出这样的错误:"指向绑定函数的指针只能用于调用函数",可以直接禁用报错波形曲线 不过也可以通过decltype(A::out) ptr = A::out;的方式解决,因为成员函数实际上是所有同类实例对象所共同拥有的, 调用的时候要指出调用的实例是因为每个实例对象的变量是单独存储的,而成员函数中需要知道this指针指向的到底是那个实例 调用的时候和前面的方式声明的指针是一致的,都是 (a.*ptr)() */ decltype(A::out)* ptr2 = a.out; /* 报错:cannot convert 'A::out' from type 'void (A::)()' to type 'void (A::**)()' 和我们之前讨论过的一样,因此可以这么理解:a.out表示的是函数类型,编译器允许用函数类型给 函数指针类型变量赋值(将void (A::)() 赋值给 void (A::*)())而不允许函数类型给函数类型 赋值(对普通函数函数指针的讨论第4行所提到的)以及其它类型的赋值(包括二维函数指针) */ decltype(A::out)* ptr3 = &(a.out); /* 报错:cannot convert 'void (A::*)()' to 'void (A::**)()' in initialization 进一步证实了a.out和&(a.out)不是相同的东西,而是编译器允许这样子赋值 */
- 因此通过分析,最好把函数名的类型认为是函数的类型而不是函数指针类型,之所以函数类型可以赋值给函数指针是因为编译器允许这么做(也许发生了隐式转换),而不是说函数名是函数指针类型
- 这样理解的话只能认为A::out 是一个特殊的表示,它不是成员函数out的函数名,如果不这么理解就会与编译器的报错给出的信息相矛盾
- 因此通过分析,最好把函数名的类型认为是函数的类型而不是函数指针类型,之所以函数类型可以赋值给函数指针是因为编译器允许这么做(也许发生了隐式转换),而不是说函数名是函数指针类型
-
理解静态成员函数可以像理解普通函数一样
-
class A{ public: static void hi(){ cout<<"hi"<<endl; } } // 声明 decltype(A::hi) ptr = A::hi; // 报错,与普通函数报错一样,A::hi是函数hi的函数名:function 'void ptr()' is initialized like a variable decltype(A::hi)* ptr1 = A::hi; // 正确,如同参考链接中所说,static 有可能就是声明了一个属于类A的全局函数,然后又将该函数声明为A的友元,所以它表现出来的属性和普通函数一致
-
注意 不可以使用decltype或auto来推导重载函数
(2) 函数指针
- 普通函数和类的成员函数的函数指针是不同的
- 以下给出普通函数的函数指针的声明和使用办法
// 以下实现对无参无返回值普通函数的函数指针定义和应用
#include <iostream>
using namespace std;
void func(){
cout<<"hello world"<<endl;
}
int main(){
// void func();的类型为 void()
// 声明一个void()型的指针ptr
void (*ptr)();
// 让ptr指向func
ptr = func; // 当然也可以是ptr = &func
// 调用ptr
ptr(); // 当然也可以是*ptr()
// 类型重命名一个void()型的函数类型FuncType1
typedef void (*FuncType1)()
FuncType ptr2 = func;
// 可以使用using 重命名函数指针类型
using Type = void (*)();
Type ptr3 = func;
// 还可以使用auto关键字
auto ptr4 = func;
// 可以使用decltype关键字
decltype(func)* ptr5 = func; // 注意decltype(func)* 获取的是函数的类型而不是函数指针类型
// 可以通过指针直接调用函数,一个通过对指针解引用调用函数
ptr(); // ok
(*ptr)(); // ok
return 0;
}
- 以下给出静态成员函数的函数指针
#include <iostream>
#include <typeinfo>
using namespace std;
class A{
public:
static void hi(){
cout<<"hi"<<endl;
}
};
int main(){
void (*ptr1)() = A::hi;
// void (A::*ptr1)() = A::hi; //cannot convert 'void (*)()' to 'void (A::*)()' in initialization, 静态函数的属性与普通函数一致
decltype(A::hi)* ptr2 = A::hi;
auto ptr3 = A::hi;
typedef void (*FuncType1)();
FuncType1 ptr4 = A::hi;
using FuncType2 = void(*)();
FuncType2 ptr5 = A::hi;
ptr1();
ptr2();
ptr3();
ptr4();
ptr5();
// A::ptr1(); // 报错,ptr1属性与普通函数一致
}
- 以下给出非静态成员函数的函数指针
#include <iostream>
#include <typeinfo>
using namespace std;
class A{
private:
string name;
public:
A(string n):name(n){};
void out(){
cout<<"hello"<<this->name<<endl;
}
};
int main(){
A a(" A");
A b(" B");
void (A::* ptr1)() = a.out; // 报错的话可以用下面的
void (A::* ptr2)() = A::out;
decltype(A::out) ptr3 = a.out; // 报错的话可以用下面的
decltype(A::out) ptr4 = A::out;
auto ptr5 = a.out; // 报错的话可以用下面的
auto ptr6 = A::out;
typedef void(A::* Functype1)();
Functype1 ptr7 = a.out; // 报错的话可以用下面的
Functype1 ptr8 = A::out;
using Functype2 = void(A::*)();
Functype2 ptr9 = a.out; // 报错的话可以用下面的
Functype2 ptr9 = A::out;
A* a_ptr = &a;
(a_ptr->*ptr1)();
}
(3) C++11:using 关键字与模板参数
-
参考C++Primer Plus (Stephen Prata) 第六版中文版"探讨C++新标准"
-
C++11之前提供了typedef 关键字来为冗长的模板参数声明设置别名,但是这样不允许部分模板参数具体化
-
// 以std::array为例,模板参数分别表示元素类型和元素个数 template<typename T> typedef std::array<T,12> arr1; // 不可取 // 必须参数全部具体化 template<typename T> using arr1 = std::array<T,12>; // 可行,允许对部分参数具体化的模板重命名
-
using 也可用在非模板的情况下,由于可读性更高可以取代typedef,
(4) C++11: 可变模板参数
参考C++Primer Plus (Stephen Prata) 第六版中文版"探讨C++新标准"
-
允许接受可变数量不同类型的参数
-
需要理解以下几个点
-
模板参数包
-
包含了n个类型参数
-
template<typename type_FIRSTARG,typename... Parameters> Parameters表示模板参数包
-
-
函数参数包
-
包含了n个不同类型的参数变量
-
template<typename type_FIRSTARG,typename... Parameters> Parameters表示函数参数包
-
-
展开参数包
-
我们声明一个可变模板参数函数,通过递归调用完成参数的使用
-
// 注意必须先声明一个无参数的版本作为递归的出口,同理如果需要对某些个数的参数的函数有修改,只需要定义对应参数个数的重载版本即可,当然递归出口可以为任意参数的 void func(){}; template<typename type_FIRSTARG,typename... Parameters> void func(type_FIRSTARG arg1,Parameters... args){ cout<<arg1<<endl; func(Parameters... args); // 间接递归,会提取Parameters和 args中的第一个值作为type_FIRSTARG和arg1 }
-
-
-
-
可变模板参数下的模板类
-
声明
-
template<typename... Parameters> class A; // 成员函数声明通之前一样
-
-
(5) 模板格式的声明
-
首先补充一下模板的相关术语
-
模板只有在发出实例化或具体化请求的地方才会生成对应的类或函数,然后参与构造,英雌模板必须与特定的模板实例化请求一起使用。
-
实例化或具体化(instantiation or specialization)
-
隐式实例化
-
通过模板声明一个或多个对象,编译器会根据模板和模板参数提供的处方,生成具体的类定义
-
vector<int> vec;
-
-
显式实例化
-
使用关键字template指出所需类型来声明,让步编译器实例化出具体的类的声明和定义
-
template class vector<int>; // 此时虽然没有创建对象,但是编译器生成对应类型的vector的类声明
-
-
显式具体化
-
有时需要不同模板参数下的类行为不同就可以使用显示实例化,实例出需要修改的类
-
// 首先声明一个通用模板,该模板用于比较传入的参数的大小 template<typename type_ARG> class Cmpr{ public: bool cmp(const type_ARG& a,const type_ARG& b){ return a>b; } } // 对特定类型字符串(const char*)比较的实际上是两个地址的大小,因此对对应的模板类显式实例化,修改细节 template<> class Cmpr<const char*>{ public: bool cmp(const char* a,const char* b){ // 指向const char*的常引用无法创建,可以更改 int len_a = 0; int len_b = 0; int i = 0; while(a[i]!='\n'){ ++len_a; ++i; } i = 0; while(b[i]!='\n'){ ++len_b; ++i; } return len_a > len_b; } }
-
当然,你还可以在特定类型的具体化模板类中添加其它通用模板中不存在的内容
-
template<> class Cmpr<const char*>{ private: int test = 100; // 通用模板中不存在的内容 public: bool cmp(const char* a,const char* b){ int len_a = 0; int len_b = 0; int i = 0; while(a[i]!='\n'){ ++len_a; ++i; } i = 0; while(b[i]!='\n'){ ++len_b; ++i; } return len_a > len_b; } void f(){ // 通用模板中不存在的内容 cout<<"hello"<<this->test<<endl; } };
-
-
(显式)部分具体化
-
允许对有多个参数的模板进行部分具体化(也就是它们拥有其它通用模板不具有的共性)
-
// 以我们的AntonaStandard::Delegate为例(源码可以看我的仓库) template<typename type_RETURN_VALUE, typename... type_PARAMETERS_PACK> class Delegate<type_RETURN_VALUE(type_PARAMETERS_PACK...)> // Delegate模板接收type_RETURN_VALUE(type_PARAMETERS_PACK...)型的函数,创建事件委托,我们重载了operator()希望返回值为 // type_RETURN_VALUE 非void的时,返回值为std::vector<remove_reference<type_RETURN_VALUE>::type>, // type_RETURN_VALUE 是void的时,返回值为void // 部分具体化返回值类型为void template<typename... type_PARAMETERS_PACK> class Delegate<void(type_PARAMETERS_PACK...)> // 以下只写出operator()在不同的具体化下的声明 // type_RETURN_VALUE 非void virtual std::vector<remove_reference<type_RETURN_VALUE>::type> operator()(type_PARAMETERS_PACK... parama_args); // type_RETURN_VALUE 是void的时,注意我们后面会解释remove_reference<type_RETURN_VALUE>::type,它是用来去除引用的 virtual void operator()(type_PARAMETERS_PACK... parama_args);
-
-
-
-
可以指定以什么样的形式接收模板参数
// 以AntonaStamdard::Delegate举例
template<typename type_RETURN_VALUE, typename... type_PARAMETERS_PACK>
class Delegate<type_RETURN_VALUE(type_PARAMETERS_PACK...)>
// 这一步操作应该是属于显式具体化(具体化的是参数列表由一堆子参数,变成了一个参数(一个函数类型),将通用模板变成和函数型模板
(6) 移除引用:将一个类型的左值或右值引用转化成该类型的左值(remove_reference)
// 使用方式
remove_reference<int&>::type a = 10; // 此时的type表示int类型
// 当然也可以脱去右值引用
remove_reference<int&&>::type b = a; // type为int类型
- 那么,remove_reference到底做了什么,实现了这个神奇的操作?
// 以下是笔者截取的部分代码
// Reference transformations.
/// remove_reference
template<typename _Tp>
struct remove_reference
{ typedef _Tp type; };
template<typename _Tp>
struct remove_reference<_Tp&>
{ typedef _Tp type; };
template<typename _Tp>
struct remove_reference<_Tp&&>
{ typedef _Tp type; };
// 可以看到,非常简单明了,通过模板参数的表达式,编译器自动选择合适的具体化形式实现
// 在模板的不同实现中,可以根据对应的参数形式推断出值类型
(7) 特殊的指针类型:void*
- void* 一般用在声明函数的时候,如果不能确认函数的返回值类型(不确定返回的指针类型),如果声明为其他类型的指针,return的时候必须进行强制转化,而void*自动接收任何指针类型。
- 看一下以下的示例:
void* func(int& i){
return &i;
}
int main(){
int b = 0;
int* a = (int*)func(b); // 没有办法隐式转换,这里需要强制类型转换
cout<<*a;
return 0;
}
/*输出
0
*/
- 当我们想要存储一系列函数指针的时候,如果不使用void*作为声明的函数指针的返回值,那么要求存储的函数返回值是要有公共基类的,必须通过多态的协变来存储,声明就会是一个很大的问题
// void*(*)(void) 类型的函数指针可以存储任何无参,返回值为指针的函数
// Base*(*)(void) 类型要求返回值必须是Base的派生类的指针,通过协变来存储,对于非class型的变量来说无法存储
(8) 使用宏来替换代码块
- 什么是宏?
- 宏指的是预编译指令,是编译器在预编译阶段进行的一系列操作
- 使用 #define 实现一个函数声明和定义代码替换
#define REPLACE(className) \
className* create_##className{ \
return new className; \
} \
REPLACE(int) // 使用宏,此时就定义了一个函数,名为int* create_int();
- 使用宏来实现一个类声明
#define REPLACE(className) \
class className{ \
\
} \
- 宏还有一个很奇特的操作: # 的功能是将其后面的宏参数进行字符串化操作,简单说就是在对它所引用的宏变量通过替换后在其左右各加上一个双引号。## 功能是在带参数的宏定义中将两个子串联接起来,从而形成一个新的子串。
#define REPLACE(className) \
function<void*(void)> f = [](){ \
return new className; \ // 在这里className是类型名
}; \
string str = #className \ // 在这里是字符串
(9) Lambda表达式的使用说明
- lambda表达式是C++11新增的语法糖,它允许匿名在函数中或者在函数外声明函数
- Lambda表达式的语法结构
[需要获取的参数](参数列表){函数体};
(10) C++多继承相关问题
-
多继承的优点
- 可以使用多个基类的虚函数或机制
- 可以实现类适配器模式,
-
多继承的缺点
- 单继承如果出现重名(签名一致)的虚函数则会重写,而多继承无法解决这个问题,如果继承的基类拥有重名成员则会造成冲突
- 如果继承的基类拥有共同的父类或祖先类则对这个祖先类成员的存储会有两份,造成空间浪费,此时应当使用虚基类来虚继承父类,消除
-
尽可能少用多继承,使用对象适配器模式替代类适配器模式
-
多重继承的构造函数调用顺序
-
#include <iostream>
using namespace std;
class Base{
public:
Base(){
cout<<"Base 构造了"<<endl;
}
virtual ~Base(){
cout<<"析构Base了"<<endl;
}
};
class C:public Base{
public:
C(){
cout<<"C 构造了"<<endl;
}
virtual ~C(){
cout<<"析构C了"<<endl;
}
};
class D:public Base{
public:
D(){
cout<<"D 构造了"<<endl;
}
virtual ~D(){
cout<<"析构D了"<<endl;
}
};
class E:public C,public D{
public:
E(){
cout<<"E 构造了"<<endl;
}
virtual ~E(){
cout<<"析构E了"<<endl;
}
};
int main(){
E e;
}
/*输出
Base 构造了
C 构造了
Base 构造了
D 构造了
E 构造了
析构E了
析构D了
析构Base了
析构C了
析构Base了
*/
// 如果继承顺序是class E:public D,public C
/*
Base 构造了
D 构造了
Base 构造了
C 构造了
E 构造了
析构E了
析构C了
析构Base了
析构D了
析构Base了
*/
-
可见构造函数调用顺序与声明顺序一致
-
成员变量访问冲突 示例:
#include <iostream>
using namespace std;
class Base{
public:
int a=1;
};
class InheritedA:public Base{
};
class InheritedB:public Base{
};
class ConcreateA:public InheritedA,public InheritedB{
};
int main(){
ConcreateA con;
cout<<con.a;
}
/*报错
request for member 'a' is ambiguous
编译器不知道通过哪个父类来访问a
可以这样访问:
con.InheritedA::a;
con.InheritedB::a;
*/
- 同时无法通过Base来访问成员a,因为Base被存储类两份,分别由InheritedA和InheritedB继承时存储,因此也无法确定是哪个Base
- 使用虚基类虚继承(将所有的继承关系都声明为虚继承)
#include <iostream>
using namespace std;
class Base{
public:
int a=1;
};
class InheritedA:virtual public Base{
};
class InheritedB:virtual public Base{
};
class ConcreateA:virtual public InheritedA,virtual public InheritedB{
};
int main(){
ConcreateA con;
cout<<con.a;
}
// 工作正常,无报错,由于虚继承,只保存了基类a=1中的
// con.InheritedA::a正常工作
// con.InheritedB::a正常工作
// con.Base::a (因为Base只存储类一份,消除了二义性)
- 多继承带来的麻烦还有很多,虽然上述可以通过con.a来访问a不会引起冲突的问题,这是因为a本质属于Base而不是InheritedA或InheritedB
- 以下的示例就会引出新问题
#include <iostream>
using namespace std;
class Base{
public:
int a=1;
};
class InheritedA:virtual public Base{
public:
int b = 99;
};
class InheritedB:virtual public Base{
public:
int b = 66;
};
class ConcreateA:virtual public InheritedA,virtual public InheritedB{
};
int main(){
ConcreateA con;
cout<<con.b;
}
// request for member 'b' is ambigious
// 必须用作用域运算符限定访问对象
// con.InheritedA::b
// con.InheritedB::b
-
多重继承可能导致函数调用的二义性
-
show在最终派生类ConcreateA中没有发生重写,那么在声明阶段就会出现一个问题:
-
no unique final overrider for ‘virtual void Base::show()’ in ‘ConcreateA’
-
-
对于单继承来说,编译器会直接将最近的基类的虚函数的重写版本作为派生类的虚函数,由于多继承,现在无法决定使用哪个个版本,补救的措施是声明出show在其中调用所需的重写版本Base版,InheritedA版或者InheritedB版
-
#include <iostream> using namespace std; class Base{ public: int a=1; virtual void show(){ cout<<"From Base!"<<endl; } }; class A:public Base{ }; int main(){ Base b; A a; cout<<(&(a.show) == &(b.show)); } // 输出1,可见a.show和b.show是同一个函数,虽然a的show没有说明,但是在声明阶段编译器就自动执行了这个操作(用Base的show代替A的show),所以不管我们是否在调用阶段明确调用的版本都会出现错误
-
#include <iostream>
using namespace std;
class Base{
public:
int a=1;
virtual void show(){
cout<<"From Base!"<<endl;
}
};
class InheritedA:virtual public Base{
public:
int b = 99;
virtual void show()override{
cout<<"From InheritedA!"<<endl;
}
};
class InheritedB:virtual public Base{
public:
int b = 66;
virtual void show()override{
cout<<"From InheritedB!"<<endl;
}
};
class ConcreteA:public InheritedA,public InheritedB{
public:
// no unique final overrider for 'virtual void Base::show()' in 'ConcreateA'
// 因为编译器需要明确知道用哪个最近的基类的重写版本代替ConcreteA的show
// virtual void show()override{
// cout<<"Ok!"<<endl;
// }
};
int main(){
ConcreateA con;
con.Base::show();
}
-
因此必须添加最终派生类的重写声明。
-
参考链接 c++ - Virtual Inheritance: Error: no unique final overrider - Stack Overflow
-
混合使用虚继承(虚基类)和非虚继承
-
考虑以下的情况 当最终派生类的基类中一个是虚基类(通过虚继承声明)另一个是非虚基类
-
-
M从虚基类A,B中一共继承了一份Base类的子对象,从C,D中分别继承了一份子对象
- M中一共有3份Base类的子对象
-
-
关于二义性的解析问题 :
- 如果一个派生类由多个非虚基类派生,且基类中含有同名成员(函数或变量)必须要使用作用域运算符 指定访问的成员,而如果由虚基类派生则不一定,如果派生类的基类中存在两个类有重名成员,对于辈分较低的(指的是同名成员的来源)可以不使用作用域运算符访问这个重名成员
- B中的同名成员show来源是Base,而A中由于重写, show成员重新定义了,其辈分相对于B的show较低
#include <iostream>
using namespace std;
class Base{
public:
virtual void show(){
cout<<"Base show!"<<endl;
}
};
class A:public virtual Base{
public:
virtual void show(){
cout<<"A show!"<<endl;
}
};
class B:public virtual Base{
};
class C:public A{
};
class D:public B,public C{
public:
void out(){
this->show(); // this->A::show();
}
};
int main(){
D d;
d.out();
}
/*输出
A show()
*/
- 参考
- C++Primer Plus 第六版6 中文版
- 笔者对多重继承的态度是保留,但不能滥用,保留是因为:有很多场景需要多重继承,例如Qt中的QGracphicsItem不是继承自QObject无法使用信号与槽机制,如果我们派生的类同时需要信号与槽机制和QGraphicsItem的功能(能被QGraphicsScene多态存储),就不得不使用多重继承(Qt中的QGraphicsObject就是这么做的)。不能滥用是因为上述所说的二义性问题造成编程的复杂性。
(11) C++11对{}运算符的扩展
- C++11允许使用{}对内置类型初始化
int x = {5};
double y {2.75};
short quar[5] {4,5,2,76,1};
/*
using Node = _node{
string name;
int val;
}
*/
Node n = {{"a,1"},{"a,2"}}; // 对结构体的赋值是没问题的
- C++11允许使用{}对用户自定义类型进行初始化
class Stump{
private:
int roots;
double weight;
public:
Stump(int r,double w):roots(r),weight(w){};
};
Stump s1(3,15.6); // C++11以前的形式
Stump s2{5,43.4}; // C++11
Stump s3 = {4,32.1}; // C++11
-
- 如果构造函数中有使用std::initializer_list 作为参数,那么只有这个构造函数可以使用{}进行初始化
(12) C++11的range-base for syntax 基于范围的for循环语法
-
C++11提供了一种遍历聚合体的语法
-
以引用方式获取元素
-
std::vector<int> vec; for(auto& member:vec){ cout<<member<<endl; // member 类型是int& }
-
-
以拷贝方式获取元素
-
std::vector<int> vec; for(auto member:vec){ cout<<member<<endl; // member 类型是int }
-
-
-
那么它的底层是怎么实现的,下面做一个实验
-
#include <iostream> #include <string> #include <vector> using namespace std; class MyVec{ private: int* lis; public: MyVec(){ this->lis = new int(10); for(auto& i:lis){ // 报错:'begin' was not declared in this scope; did you mean 'std::begin'? i = 6; } } ~MyVec(){ delete lis; } }; int main(){ MyVec v; for(auto i:v){ // 报错:'begin' was not declared in this scope; did you mean 'std::begin'? cout<<i; } }
#include <iostream> #include <string> #include <vector> using namespace std; using Node = struct node_ { int val_1; string name; }; int main(){ Node array_node[] = {{1,"a"},{2,"b"},{3,"c"}}; int a[10] = {0,1,2,3,4,5,6,7,8,9}; vector<int> vec = {9,8,7,6,5,4,3,2,1,0}; string str = "hello_world!"; for(auto i:a){ cout<<i<<" "; } cout<<endl; for(auto i:array_node){ cout<<"{"<<i.name<<","<<i.val_1<<"} "; } cout<<endl; for(auto i:vec){ cout<<i<<" "; } cout<<endl; for(auto i:str){ cout<<i<<" "; } cout<<endl; } /* 0 1 2 3 4 5 6 7 8 9 {a,1} {b,2} {c,3} 9 8 7 6 5 4 3 2 1 0 h e l l o _ w o r l d ! */
-
可见并不高深,器底层是通过迭代器实现的,默认通过begin函数获取首迭代器,end函数获取尾迭代器,第二个实验中可以看出,数组是允许没有迭代器而使用for语句进行迭代的,但是自定义的类型甚至new 出来的基本类型都不能直接迭代,必须依靠begin(),end()获取迭代器,并且迭代器必须重载了operator++
-
几个注意事项
- 禁止在for(auto …){}中对容器进行插入和删除操作,这样会导致迭代器失效,从而出现异常
- 暂时没有官方语法进行逆向迭代,如果一定要实现可以参考这篇回答
- 参考上方回答的代码,我在仓库中添加了一个工具库,可以将rbegin() 和rend()包装成含有begin()和end()的模板结构体实现逆向迭代
-
(13) C++函数与数组传参
- C语言中数组是被当作指针进行参数传递的,这个过程会导致数组的大小信息损失掉
void getlen(int array[] ){
cout<<sizeof(array)/sizeof(array[0]);
}
void getlen_plus(int array[10]){
cout<<sizeof(array)/sizeof(array[0]);
}
- 对于int类型的数组如果进行值传递,其形参是是int*类型的,这个赋值操作会导致大小信息损失
- 必须通过引用:
void getlen_pro_plus(int (&array)[3]){
cout<<sizeof(array)/sizeof(array[0]);
}
int a[] = {1,2,3};
getlen(a);
// 虽然这么写无异于脱裤子放p,不过后面我们谈及将数组作为模板参数时会非常有用
(14) 将数组作为模板参数
- 对于模板来说,如果没有进行显式指定,编译器选择函数版本时会进行最优匹配
template<typename type_ele>
void getlen(type_ele(&array)[]){
。。。
}
- 只有无边界数组类型的实参才能传入
int a[10];
getlen(a); // 出错,无匹配
getlen((char (&)[])a); // 强制类型转换后正确
- 当然,依照最优匹配原则,以及前面讲的数组进行值传递时形参是对应类型指针,以下的示例会发生匹配出错的问题
template<typename type_ele,int size_array>
void out1(type_ele arr[size_array]){
// 形参退化成是type_ele*,无法与int[3]匹配
// 当然强制转化为type_ele*也不行,因为没有size_array参数
for(int i = 0;i<size_array;++i){
cout<<arr[i]<<endl;
}
}
int a[] = {1,2,3};
out1(a); // no matching function for call to 'out1(int[3])'
out1<int,3>(a); // 使用显式指定,正常工作
- 正确的做法:使用引用
// 以下模板函数可以输出任意类型的数组
template<typename type_ele,int size_array>
void out(type_ele (&arr)[size_array]){
for(int i = 0;i<size_array;++i){
cout<<arr[i]<<endl;
}
}
int main() {
int a[] = {1,2,3};
const char* s[] = {"hello","world!","!"};
out(a);
out(s);
return 0;
}
(15) 关于运算符new和delete的重载
对于这两个运算符重载的风险程度较高,如果疏忽可能会导致一种更为严重的内存泄露——幽灵指针
-
可以重载运算符new 和 delete,用于完成一些功能
-
注意运算符内部需要调用全局运算符new,和delete用于完成申请内存和删除内存的操作
- 一般情况下请不要对全局new运算符和delete运算符重载 ,除非你知道你在做什么,这样所有的类的new和delete操作都会执行这个函数
-
以下是实现单例模式的示例,使用到了上述的重载
- 只重载delete
#include <iostream>
#include <vector>
#include <time.h>
using namespace std;
int init_v = 10;
class Singleton{
private:
static Singleton* instance;
Singleton(){
this->value = init_v;
init_v+=9;
};
int value;
public:
static Singleton* getInstance(); // 将构造函数声明为private只允许通过getInstance获取实例
void operator delete(void* ptr){
cout<<"正在删除"<<endl;
if(Singleton::instance != nullptr){
::operator delete (Singleton::instance);
Singleton::instance = nullptr;
}
}
void show(){
cout<<"value:"<<this->value<<endl;
}
~Singleton(){
cout<<"准备删除"<<endl;
}
};
Singleton* Singleton::getInstance(){
if(Singleton::instance == nullptr){
Singleton::instance = new Singleton();
}
return Singleton::instance;
}
Singleton* Singleton::instance = nullptr;
int main(){
Singleton* ptr = Singleton::getInstance();
ptr->show();
Singleton* ptr_2 = Singleton::getInstance();
ptr_2->show();
delete ptr_2;
ptr_2 = Singleton::getInstance();
ptr_2->show();
delete ptr_2;
return 0;
}
/*输出
value:10
value:10
准备删除
正在删除
value:19
准备删除
正在删除
*/
- 重载new和delete
#include <iostream>
#include <vector>
#include <time.h>
using namespace std;
int init_v = 10;
class Singleton{
private:
static Singleton* instance;
int value;
public:
Singleton(){
this->value = init_v;
};
void* operator new(size_t size){
if(Singleton::instance == nullptr){
init_v+=9; // 除非分配了新内存,否则用于初始化value的全局变量不会变
Singleton::instance = ::new(Singleton);
}
return Singleton::instance;
}
void operator delete(void* ptr){
cout<<"正在删除"<<endl;
if(Singleton::instance != nullptr){
::operator delete (Singleton::instance);
Singleton::instance = nullptr;
}
}
void show(){
cout<<"value:"<<this->value<<endl;
}
~Singleton(){
cout<<"准备删除"<<endl;
}
};
Singleton* Singleton::instance = nullptr;
int main(){
Singleton* ptr = new Singleton;
ptr->show();
Singleton* ptr_2 = new Singleton;
ptr_2->show();
delete ptr_2;
ptr_2 = new Singleton;
ptr_2->show();
delete ptr_2;
return 0;
}
/*
value:19
value:19
准备删除
正在删除
value:28
准备删除
正在删除
*/
Qt部分
- 本期暂时没有Qt部分的踩坑