11 C++11新特性
1 auto编译期间通过初值自动推导类型
decltype 编译期间通过初值自动推导类型
auto var = val1 + val2;
decltype(val1 + val2) var1 = 0;
关于auto和decltype具体的看自己写的笔记
auto 只能保证变量的基本类型与等号右边的变样一样,第二属性(顶层const,valatile, &/&&)不能保证相同
2 lambda匿名函数,常用来做容器需要的比较或哈希函数的实参
[capture list] (parameter list) -> return type
{
function body;
};
//capture list:捕获列表,指 lambda 所在函数中定义的局部变量的列表,通常为空。
//return type、parameter list、function body:分别表示返回值类型、参数列表、函数体,和普通函数一样
int main()
{
int arr[4] = {4, 2, 3, 1};
//对 a 数组中的元素进行升序排序
// []用来捕获上下文或者作用域中变量到lamda表达式函数体中使用,[=]意思是全部按值捕获变量,[&]全部按引用捕获变量
sort(arr, arr+4, [=](int x, int y) -> bool{ return x < y; } );
for(int n : arr){
cout << n << " ";
}
return 0;
}
3 范围for
for (declaration : expression){
statement
}
char arr[]=”hello word”;
for(char c:arr)
cout<
declaration:此处定义一个变量,序列中的每一个元素都能转化成该变量的类型,常用 auto 类型说明符。
4 右值和move(具体看自己的笔记)
5 delete和default
delete 函数:= delete 表示该函数不能被调用。
default 函数:= default 表示编译器生成默认的函数,例如:生成默认的构造函数。
A() = default; // 表示使用默认的构造函数
~A() = default; // 表示使用默认的析构函数
A(const A &) = delete; // 表示类的对象禁止拷贝构造
A &operator=(const A &) = delete; // 表示类的对象禁止拷贝赋值
6 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 类不可派生
…
};
7 constexpr
先介绍常量表达式,字面量(1,2,3)的数字计算是常量表达式,匿名枚举、switch-case 结构中的 case 表达式等(我们在声明一个数组int a[长度],这个长度必须是个常量不能是变量),常量表达式的计算往往发生在程序的编译阶段,这可以极大提高程序的执行效率,因为表达式只需要在编译阶段计算一次,节省了每次程序运行时都需要计算一次的时间。
constexpr 关键字的功能是使指定的常量表达式获得在程序编译阶段计算出结果的能力,而不必等到程序运行阶段。C++ 11 标准中,constexpr 可用于修饰普通变量、函数(包括模板函数)以及类的构造函数。
注意获得在编译阶段计算出结果的能力,并不代表 constexpr 修饰的表达式一定会在程序编译阶段被计算,具体的计算时机还是编译器说了算。
constexpr修饰普通变量
constexpr int num = 1 + 2 + 3;// 使用 constexpr 修改普通变量时,变量必须经过初始化且初始值必须是一个常量表达式
int url[num] = {1,2,3,4,5,6};
另外需要重点提出的是,当常量表达式中包含浮点数时,考虑到程序编译和运行所在的系统环境可能不同,常量表达式在编译阶段和运行阶段计算出的结果精度很可能会受到影响,因此 C++11 标准规定,浮点常量表达式在编译阶段计算的精度要至少等于(或者高于)运行阶段计算出的精度。
constexpr修饰函数的返回值
这样的函数成为常量表达式函数,一个函数要想成为常量表达式函数需要满足以下4个要求
1 整个函数的函数体中,除了可以包含 using 指令、typedef 语句以及 static_assert 断言外,只能包含一条 return 返回语句
constexpr int display(int x) {
int ret = 1 + 2 + x;// 此函数无法通过编译,因为包含除return外的语句
return ret;
}
constexpr int display(int x) {
//可以添加 using 执行、typedef 语句以及 static_assert 断言
return 1 + 2 + x;// 正确 只有一条return语句
}
2 函数返回值类型不能是void (常量表达式必须有类型)
3 常量表达式函数在使用前,必须已经有完整定义
普通的函数调用只需要提前写好该函数的声明部分即可,但常量表达式函数在使用前,必须要有该函数的定义。
//普通函数的声明
int noconst_dis(int x);
//常量表达式函数的声明
constexpr int display(int x);
//常量表达式函数的定义
constexpr int display(int x){
return 1 + 2 + x;
}
int main()
{
//调用常量表达式函数
int a[display(3)] = { 1,2,3,4 };
cout << a[2] << endl;
//调用普通函数
cout << noconst_dis(3) << endl;
return 0;
}
//普通函数的定义
int noconst_dis(int x) {
return 1 + 2 + x;
}
4 return 返回的表达式必须是常量表达式,如果想在程序编译阶段获得某个函数返回的常量,则该函数的 return 语句中就不能包含程序运行阶段才能确定值的变量。
在常量表达式函数的 return 语句中,不能包含赋值的操作(例如 return x=1 在常量表达式函数中不允许的)。另外,用 constexpr 修改函数时,常量表达式函数是支持递归的
constexpr修饰类构造函数
constexpr struct myType {// 错误 不能直接用constexpr 修饰结构体或类
};
struct myType {
constexpr myType(char *name,int age):name(name),age(age){};// 正确
// 要求该构造函数的函数体必须为空
const char* name;
int age;
//其它结构体成员
};
int main()
{
constexpr struct myType mt { "zhangsan", 10 };//采用初始化列表的方式为各个成员赋值时,必须使用常量表达式
cout << mt.name << " " << mt.age << endl;
return 0;
}
前面提到,constexpr 可用于修饰函数,而类中的成员方法完全可以看做是“位于类这个命名空间中的函数”,所以 constexpr 也可以修饰类中的成员函数,只不过此函数必须满足前面提到的 4 个条件。C++11 标准中,不支持用 constexpr 修饰带有 virtual 的成员函数。
constexpr修饰模板函数
constexpr 可以修饰模板函数,但由于模板中类型的不确定性,因此模板函数实例化后的函数是否符合常量表达式函数的要求也是不确定的,C++11 标准规定,如果 constexpr 修饰的模板函数实例化结果不满足常量表达式函数的要求,则 constexpr 会被自动忽略,即该函数就等同于一个普通函数。
//模板函数
template<typename T>
constexpr T dispaly(T t){
return t;
}
constexpr struct myType stu{ "zhangsan", 10 };
constexpr struct myType ret = dispaly(stu);
constexpr int ret1 = dispaly(10);// ok 常量表达式函数
12 面向对象三大特性
封装:实现过程和数据封装成类
继承:子类继承父类的特征和行为,子类有父类的非 private 方法或成员变量,子类可以对父类的方法进行重写,增强了类之间的耦合性,但是当父类中的成员变量、成员函数或者类本身被 final 关键字修饰时,修饰的类不能继承,修饰的成员不能重写或修改。
多态:父类(爷爷类/祖先类)指针(引用)指向子类对象,通过父类(爷爷类/祖先类)指针可以调用子类中在父类(爷爷类/祖先类)里定义的同名同参数虚函数(覆盖)
13 重载(overload),隐藏(overwrite),覆盖(override)
Overload(重载):在C++程序中,可以将语义、功能相似的几个函数用同一个名字表示,但参数(包括类型、顺序不同),即函数重载。
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;参数的个数,类型,顺序不同都符合重载标准,或者函数是否被const声明也算(返回值中有个例外,父类函数返回父类的指针或引用,子类函数返回子类的指针或引用这种情况也ok)
(4)virtual 关键字可有可无。
Override(覆盖):是指子类函数覆盖父类(爷爷类/祖先类)函数—多态行为,特征是:
(1)不同的范围(分别位于子生类与父类);
(2)函数名、参数列表、返回值类型,const声明以及其它各种前后缀都与父类中函数相同()
(3)父类(爷爷类/祖先类)函数必须有virtual 关键字。
如果类中有虚函数(继承来的也算),每个类都有一个虚函数表(vtable 虚表属于类)记录了这个类中的所有虚函数的地址,编译器为类中含虚函数的对象提供一个虚表指针(vptr 每个类中都有虚表指针),在程序运行时根据对象类型将此指针指向对应虚表。
每个对象调用的虚函数都是通过虚表指针来索引的,也就决定了虚表指针的正确初始化是非常重要的。换句话说,在虚表指针没有正确初始化之前,我们不能够去调用虚函数。在对象构造函数中进行虚表指针的初始化。
vptr虚表指针是一个指针,在类的构造函数中创建生成(虚表指针存放在对象的内存空间中最前面的位置,这是为了保证正确取到虚函数的偏移量),并且只能用this指针来访问它,因为它是类的一个成员变量(非显示的),并且vptr指向保存虚函数地址的vtable。
在构造子类对象时,要先调用父类(爷爷类/祖先类)的构造函数,此时编译器只“看到了”父类(爷爷类/祖先类),并不知道后面是否后还有继承者,它初始化父类(爷爷类/祖先类)对象的虚表指针,该虚表指针指向父类(爷爷类/祖先类)的虚表(此时子类虚表指针也是指向父类(爷爷类/祖先类)虚表)。当执行子类的构造函数时,子类对象的虚表指针被初始化,指向自身的虚表。这样的行为保证了在构造时不会出现多态现象(也可以用this指针的类型来解释)。
对于虚函数调用来说,每一个对象内部都有一个虚表指针,该虚表指针被初始化为本类的虚表。所以在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针是固定的,所以呢,才能实现动态的对象函数调用。
父类(爷爷类/祖先类)指针指向子类对象,子类对象生产后其中内部的虚表指针指向的是子类的虚表,父类(爷爷类/祖先类)指针指向子类对象,但是调用的非父类中的virtual 声明函数,调用的是父类(爷爷类/祖先类)中的同名函数(调用由指针类型决定)
1 如果父类有3个虚函数,那么父类的虚表中就有三项(虚函数地址),子类也会有虚表,至少有三项,如果覆盖了父类相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果子类有自己的虚函数,那么虚表中就会添加该项。子类的虚表中虚函数地址的排列顺序和父类的虚表中虚函数地址排列顺序相同。
2 子类覆盖父类(爷爷类/祖先类)虚函数,是否要在函数定义前声明virtual?
不用(可加可不加,推荐加,程序看得更清楚些),当父类成员函数被声明为虚函数后,其子类中同名,同参数,同前后缀的函数自动成为虚函数被加到子类的虚表中,覆盖虚表中的父类函数。
3 函数调用到底调的时哪个函数?
所有不满足upcast调用虚函数这个行为的所有其他调用行为,编译器都会在编译期根据对象的静态类型来选择函数。静态类型就是对象的类类型或对象指针/引用类型 而非引用或指针指向的那个对象的类型(动态类型)
4 this指针的类型问题
在类X的成员函数中,this指针是X*类型的(若成员函数有cv限定则this类型为cv X),即当子类调用父类(爷爷类/祖先类)函数时,**在父类函数中this是父类的(this作为函数形参传入 形参类型是父类),而我们传入的对象地址是子类对象的地址,即父类 this=子类对象地址**; — 完成了up_cast(多态的要求),在父(爷爷类/祖先类)成员函数中如果遇到子类覆盖的虚函数vfun(this->vfun,vfun子类中有覆盖),将发生多态行为,父成员函数中将会调用子类的vfun。以及当子类指针指向父类(爷爷类/祖先类)对象时的down_cast会引发的风险是同理的(调的函数是哪个类的,这个函数中的this指针就是那种类型)
一个tips,关于父类(爷爷类/祖先类)指针指向子类数组
child c[5];
parent* p = c;
p++;// 注意并不会指向c[1],因为加后的p地址=c+sizeof(parent)!=c+sizeof(child),父类指针并非指向子类对象首地址
p->print();// 不会实现多态,还会报错
一个tips 父类(爷爷类/祖先类)析构函数定义为虚函数,因为因为不是定义为虚函数时,父类型(爷爷类/祖先类)的指针去访问子类,在析构此指针时候,子类的析构函数将不会被调用,这样会出现内存泄漏。
一个tips,多继承的虚函数表结构(https://www.cnblogs.com/raichen/p/5744300.html 写的非常清楚,描述了对象模型)
下图中的所有函数都是虚函数
子类的虚函数表,子类继承自3个父类,在子类中就存在3个虚表指针,分别指向3个自己的虚表(注意并非指向父类的虚表,此子类由于多继承的关系拥有3张虚表),3个虚表指针按照继承声明顺序排列,并且从下图中可以观察到子类的新虚函数插入到继承顺序中第一个虚表中,子类重写父类的虚函数(Derive::fun1)对所有含同名虚函数的父类虚函数进行覆盖(对应虚表位置中的虚函数就改为子类的这个)
一个tips 析构函数定义成虚函数是为了防止内存泄漏,因为当父类(爷爷类/祖先类)的指针或者引用指向或绑定到派生类的对象时(Base p = new Derive()),如果未将父类的析构函数定义成虚函数,会调用父类的析构函数,那么只能将父类的成员所占的空间释放掉,子类中特有的就会无法释放内存空间导致内存泄漏。 原因在上面的图上有描述,当使用指针或引用调用函数,会去检查调用的函数是否为虚函数,如果是虚函数会进行是否满足up_cast条件的检查,*调用非虚函数,如果是父子类汇中都有的同名同参数函数,则根据引用\指针 类型调用那个类中相应的函数(即在上面的情况中调用的是父类的函数)。
tips 父/祖先指针or引用 p = 子/后代对象; 满足up_cast使用p可以调用后代中的虚函数(不满足up_cast或调用的不是虚函数则调用的都是静态类型父/祖先中的对应函数), 用父/祖先指针->成员变量,能访问到的成员变量 只有父/祖先类中已经定义的成员变量(这个变量的值是子/后代对象中保存的值),子/后代类中的成员变量这样是访问不到的。
例子
class Base{
public:
virtual void func();
virtual void func(int);
};
void Base::func(){
cout<<"void Base::func()"<<endl;
}
void Base::func(int n){
cout<<"void Base::func(int)"<<endl;
}
//派生类Derived
class Derived: public Base{
public:
void func();
void func(char *);
};
void Derived::func(){
cout<<"void Derived::func()"<<endl;
}
void Derived::func(char *str){
cout<<"void Derived::func(char *)"<<endl;
}
int main(){
Base *p = new Derived();
p -> func(); //输出void Derived::func() 多态生效
p -> func(10); //输出void Base::func(int) 调用静态类型(对象的类类型或对象指针/引用类型)类中的对应函数
p -> func("http://c.biancheng.net"); //compile error调用静态类型类中的对应函数,但是此函数在静态类型中不存在所以报错
return 0;
}
Overwrite(隐藏):是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果子类的函数与父/祖先类的函数同名,但是参数不同。此时,不论有无virtual关键字,父类的所有同名函数将被隐藏(注意别与重载混淆)。
(2)如果子类的函数与父/祖先类的函数同名,并且参数也相同,但是父/祖先类函数没有virtual关键字。此时,父类的所有同名函数被隐藏(注意别与覆盖混淆)。
只要同名函数,不管参数列表是否相同,基类函数都会被隐藏(除了覆盖的那种情况)
可以用对象.父类名(爷爷类/祖先类)::同名隐藏函数()这种形式调用父类(爷爷类/祖先类)中被隐藏的函数
隐藏导致的结果就是,被隐藏的函数在子类中不存在无法直接使用,只能通过父类名(爷爷类/祖先类)::同名隐藏函数()来调用
一个tips
class Base
{
public:
virtual void fun(int tmp=1) { cout << "Base::fun(int tmp) : " << tmp << endl; }
};
class Derived : public Base
{
public:
void fun(int tmp=2) { cout << "Derived::fun(int tmp) : " << tmp << endl; } };
int main()
{
Base *p = new Derived();
p->fun(); // Derived::fun(int tmp) : 1 有点怪对吧
//当缺省参数和虚函数一起出现的时候情况有点复杂,极易出错。我们知道,虚函数是动态绑定的,
//但是为了执行效率,缺省参数是静态绑定的。对于这个特性,估计没有人会喜欢。
//所以,永远记住:“绝不重新定义继承而来的缺省参数!!(Never redefine function’s inherited default parameters value.)”
return 0;
}
13 sizeof和strlen的区别
strlen 是头文件
strlen 测量的是字符串的实际长度(其源代码如下),以 \0 结束。而 sizeof 测量的是字符数组的分配大小。
char arr[10] = "hello";
cout << strlen(arr) << endl; // 5
cout << sizeof(arr) << endl; // 10
若字符数组 arr 作为函数的形参,sizeof(arr) 中 arr 被当作字符指针来处理,strlen(arr) 中 arr 依然是字符数组
void size_of_char_array(char (&arr)[10])//想要sizeof将arr当作数组 需要传数组的引用
{
cout << sizeof(arr) << endl; // 10
cout << strlen(arr) << endl;
}
14 lambda(11补充)
lambda匿名函数,常用来做容器需要的比较或哈希函数的实参
lambda匿名函数本质上是一个重载了operator()的函数形式的类对象
[capture list] (parameter list) mutable(加了以后可以对 值传递的捕获变量进行修改,因为是值传递修改了也不会影响外部,但是不加mutable对值传递捕获变量无法修改) throw(类型—Lambda表达式可以抛出指定类型的异常) -> return type
{
function body;
};
capture list:捕获列表,指 lambda 所在函数中定义的局部变量的列表,通常为空([]空白表示不使用任何外部参数,仅可使用参数列表中的参数)。只能捕获lambda定义所处的 非静态局部变量,对于全局变量和局部静态变量在lambda函数体内可直接使用,&表示引用传递,=表示值传递(lambda 函数创建时会调用,拷贝构造函数去构造这个捕获的局部变量)
SourceInsight—C++ insight 软件观察编译器编译的源代码https://cppinsights.io/ 超好用
[] 未定义任何变量,不能访问lambda之外定义的变量
[x, &y] x是通过值复制来访问的,y通过引用访问
[&] 能通过引用访问外部所有变量
[=] 外部变量都是通过值复制来访问
[&, x] x通过值复制访问,其他变量通过引用访问
[=, &z] z通过引用访问,其他变量通过值捕获
捕获变量a前加了&表示引用传递,b前没加则默认值传递
如果我们定义了parameter list参数列表如
auto m3=[&a,b](int x,int y)mutable ->bool{ a=x;b=y; return a<b;};
则在__lambda_7_15类中会多一个对()符号的重载
c++的lambda 可以捕获this指针,使lambda可以在自定义的function内使用类的成员函数和成员变量,这是因为捕获this后隐式的在成员变量前加了this
但是需要注意的是,这里捕获this,不是以一种拷贝的方式,更像是一种引用(或者别名,描述可能不准确),当在外面这个类的生命周期结束时,lambda内部还在调用这个类的成员函数,那么就会出错(https://www.cnblogs.com/wangshaowei/p/14696424.html)
class Example{
public:
Example() : m_var(10) {}
void func(){
// this,=,&都可以在成员函数中的lambda捕获到this
[this]() { return m_var; }(); // IIFE
}
private:
int m_var;
};
引用捕获可能带来悬挂引用
常见于使用Lambda表达式使用引用捕获某个局部变量,而调用Lambda表达式时,局部变量已经被清理导致,捕获的引用指向被清理的内存空间产生悬挂引用
return type、parameter list、function body:分别表示返回值类型、参数列表、函数体,和普通函数一样
int main()
{
int arr[4] = {4, 2, 3, 1};
//对 a 数组中的元素进行升序排序
// []用来捕获上下文或者作用域中变量到lamda表达式函数体中使用,[=]意思是全部按值捕获变量,[&]全部按引用捕获变量
sort(arr, arr+4, [=](int x, int y) -> bool{ return x < y; } );
for(int n : arr){
cout << n << " ";
}
return 0;
}
容器中的比较函数使用lambda需要像如下
auto cmp = [](const A& a,const A& b){return a.data< b.data;};// 大顶堆
/**
* 需要把lambda表达式作为优先队列参数进行初始化
* 并且指定priority_queue的模板实参,decltype(cmp),c++11 declare type,声明类型
* 可以认为是确定函数的类型
* bool (const student & a,const student & b)
**/
priority_queue<A,vector<A>,decltype(cmp)> q(cmp);
/*使用函数对象来定义这个比较函数原型*/
//lambda 函数来初始化函数对象
priority_queue<A,vector<A>,function<bool(const A&,const A&)>> que2(cmp);
lambda表达式还可以作为函数的参数使用。用于函数回调
template <typename F>
void funcA(int a, F pFunc)
{
cout << "a = " << a << endl;
pFunc(a + 1);
}
int main()
{
funcA(10, [=](int b){
cout << "b = " << b << endl;
});
return 0;
}
15 explicit的作用 避免编译器进行隐式类型转换
用来声明类构造函数是显示调用的,而非隐式调用,可以阻止调用构造函数时进行隐式转换。只可用于修饰单参构造函数,因为无参构造函数和多参构造函数本身就是显示调用的,再加上 explicit 关键字也没有什么意义(错误!!! 在C++11就已经满足多参数的explicit )。
class A {
private:
int m_i;
public:
A(int a) : m_i(a)
{
cout << "A(int a) 调用" << endl;
}
A& operator= (const A& a)
{
cout << "operator= (const A& a) 调用" << endl;
this->m_i = a.m_i;
return *this;
}
};
int main()
{
A a(10);
cout << "-------------------------------" << endl;
a = 11;
}
//打印结果:
//A(int a) 调用
//-------------------------------
//A(int a) 调用 //调用A(11) 发生了对匿名对象的隐式转换,将int(11)->A类型的匿名对象
//operator= (const A& a) 调用
下面用一个cppreference的例子解释加了explicit后的结果(c++11),以及这段代码在cppinsights下c++17的编译结果
struct A
{
A(int) { } // converting constructor
A(int, int) { } // converting constructor (C++11)
operator bool() const { return true; }
};
struct B
{
explicit B(int) { }
explicit B(int, int) { }
explicit operator bool() const { return true; }
};
int main()
{
A a1 = 1; // OK: copy-initialization selects A::A(int) A a1 = A(1);
A a2(2); // OK: direct-initialization selects A::A(int)
A a3 {4, 5}; // OK: direct-list-initialization selects A::A(int, int)
A a4 = {4, 5}; // OK: copy-list-initialization selects A::A(int, int) A a4 = A{4,5};
A a5 = (A)1; // OK: explicit cast performs static_cast A a5 = static_cast<A>(1);// static_cast会直接构造一个A(1)
if (a1) ; // OK: A::operator bool() if(a1.operator bool())
bool na1 = a1; // OK: copy-initialization selects A::operator bool() bool na1 = a1.operator bool();
bool na2 = static_cast<bool>(a1); // OK: static_cast performs direct-initialization bool na2 = static_cast<bool>(a1.operator bool());
// B b1 = 1; // error: copy-initialization does not consider B::B(int)
B b2(2); // OK: direct-initialization selects B::B(int)
B b3 {4, 5}; // OK: direct-list-initialization selects B::B(int, int)
// B b4 = {4, 5}; // error: copy-list-initialization does not consider B::B(int,int)
B b5 = (B)1; // OK: explicit cast performs static_cast B b5 = static_cast<B>(1);
if (b2) ; // OK: B::operator bool()
// bool nb1 = b2; // error: copy-initialization does not consider B::operator bool()
bool nb2 = static_cast<bool>(b2); // OK: static_cast performs direct-initialization
}
16 c++中的static
在 C 语言中,使用 static 可以定义局部静态变量、外部静态变量、静态函数
在 C++ 中,使用 static 可以定义局部静态变量、外部静态变量、静态函数、静态成员变量和静态成员函数。因为 C++ 中有类的概念,静态成员变量、静态成员函数都是与类有关的概念。
隐藏:static 作用于全局变量和函数,改变了全局变量和函数的作用域,使得全局变量和函数只能在定义它的文件中使用,在源文件中不具有全局可见性。(注:普通全局变量和函数具有全局可见性,即其他的源文件也可以使用。)
静态数据成员是属于整个类的,而不是属于某个对象。即不管实例多少个对象,它们都公用一个静态数据成员(包括子类的对象也和父类对象一起只拥有这一个静态数据成员)。静态函数成员也是一样,属于一个类,在调用时不需要对象就可以调用。
静态数据成员同样受到private、public、protected 访问规则的限制,初始化都一样是在类外初始化的。
类的静态成员函数中只能访问静态成员变量或者静态成员函数(不能访问外部函数, 没有this指针),不能将静态成员函数定义成虚函数,以及不能对函数加CV限定。对于一个类中的const修饰的成员函数,this指针相当于 类名 const , 而对于非const成员函数,this指针相当于 类名,static成员函数没有this指针,所以使用CV来修饰static成员函数没有任何意义。因为没有this指针,所以没办法通过this指针得到对象的虚表指针,无法访问虚函数。
静态数据成员(static int b )必须在类外初始化(int 类名::b=100—-类外初始化时不要出现 static 关键字和private、public、protected 访问规则)不在类外赋值默认为0,这是因为静态数据成员不属于任何一个对象,而是属于整个类的(静态数据成员的生命周期比一个具体的类对象更长一些,在类对象存在之前就已经存在)。
class A
{
public:
static int s_var;
int var;
// 静态成员变量可以作为成员函数的参数,而普通成员变量不可以
void fun1(int i = s_var); // 正确,静态成员变量可以作为成员函数的参数
void fun2(int i = var); // error: invalid use of non-static data member 'A::var'
// 静态数据成员的类型可以是所属类的类型,而普通数据成员的类型只能是该类类型的指针或引用
static A s_var; // 正确,静态数据成员
A var; // error: field 'var' has incomplete type 'A'
A *p; // 正确,指针
A &var1; // 正确,引用
};
17 c++中的const
const 修饰成员变量,定义成 const 常量,相较于宏常量,可进行类型检查,节省内存空间,提高了效率。
const 修饰函数参数,使得传递过来的函数参数的值不能改变。
const 修饰成员函数,使得成员函数不能修改任何类型的成员变量(mutable 修饰的变量除外),也不能调用非 const 成员函数,因为非 const 成员函数可能会修改成员变量。
class A
{
public:
A(int i_):i(i_){}
void change() const{
i = 1;// 正确 常成员函数可以修改mutable 修饰的非 常成员变量
}
mutable const int i;// C++中const的语义是保证物理常量性,但通过mutable关键字可以支持一部分的逻辑常量性。mutable也是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中
};
不加const正常的成员函数中的this指针是指针常量(指针指向不能改),const成员函数中的this是指向常量的指针常量(const 类型名 * const this 内容和指向都不能改)
const 成员变量只能在类内声明、定义,在构造函数初始化列表中初始化。
const 成员变量只在某个对象的生存周期内是常量,对于整个类而言却是可变的,因为类可以创建多个对象,不同类的 const 成员变量的值是不同的。因此不能在类的声明中初始化 const 成员变量,类的对象还没有创建,编译器不知道他的值。
struct A
{
const int a = 5;// 注给了一个默认值(c++11开始可以在类中给初值)
A(int a) : a(a) {}// 对象被定义出来的时候(构造的时候) 初始化const成员变量
};
对象的常量性可以分为两种:物理常量性(即每个bit都不可改变)和逻辑常量性(即对象的表现保持不变)。C++中采用的是物理常量性,例如下面的例子a是const对象,则对a的任何成员进行赋值都会被视为error,但如果不改动ptr,而是改动ptr指向的对象,编译器就不会报错。这实际上违背了逻辑常量性,因为A的表现已经改变了,const最准确的表达是只读比常量的表达更准确
struct A {
int *ptr;
};
int k = 5, r = 6;
const A a = {&k};
a.ptr = &r; // !error
*a.ptr = 7; // no erro
define 是在编译预处理阶段进行替换,const 变量值可在编译期确定也可以在运行期确定
const int COMPILE_CONST = 10;// 编译期确定
const int RunTimeConst = cin.get();// 运行时确定
int a1[COMPLIE_CONST]; // ok in C++
int a2[RunTimeConst]; // !error in C++
constexpr才是编译时就确定的值
constexpr:告诉编译器我可以是编译期间可知的,尽情的优化我吧。
const:(只读)告诉程序员没人动得了我,放心的把我传出去;或者放心的把变量交给我,我啥也不动就瞅瞅。
constexpr是一种比const 更严格的束缚, 它修饰的表达式本身在编译期间可知, 并且编译器会尽可能的 evaluate at compile time. 在constexpr 出现之前, 可以在编译期初始化的const都是implicit constexpr(明确的常量表达式)。
补充一条:不能在类中定义宏,但可以在类中定义const。宏如果定义在类内则在类之后定义的函数,变量等仍可以访问此宏,这样就使得类失去了封装性。一般在类中使用enum{}代替宏定义。
静态常量 static const 我能想到的用处就是,类中共享的常量 可以设成静态常量
18 define与type define的区别
#define 作为预处理指令,在编译预处理时进行替换操作,不作正确性检查,只有在编译已被展开的源程序时才会发现可能的错误并报错。typedef 是关键字,在编译时处理,有类型检查功能,用来给一个已经存在的类型一个别名。
功能:typedef 用来定义类型的别名,方便使用。#define 不仅可以为类型取别名,还可以定义常量、变量、编译开关等。
typedef:
如果放在所有函数之外,它的作用域就是从它定义开始直到文件尾;
如果放在某个函数内,定义域就是从定义开始直到该函数结尾;
#define:不管是在某个函数内,还是在所有函数之外,作用域都是从定义开始直到整个文件结尾。
#define INTPTR1 int * // 字符替换将接下来代码文件中的INTPTR1字符 全部替换为int*
typedef int * INTPTR2; // 给int*取别名INTPTR2
INTPTR1 p1, p2; // p1: int *; p2: int
INTPTR2 p3, p4; // p3: int *; p4: int *
int var = 1;
const INTPTR1 p5 = &var; // 相当于 const int * p5; 常量指针
const INTPTR2 p6 = &var; // 相当于 int * const p6; 指针常量
//用宏实现大小比较,宏展开传入参数,需要用()保证操作符优先级,保证实现逻辑是正确的
#define MAX(X, Y) ((X)>(Y)?(X):(Y))
#define MIN(X, Y) ((X)<(Y)?(X):(Y))
19 inline内联函数
内联函数,像普通函数一样被调用,但是在调用时并不通过函数调用的机制而是直接在调用点处展开(直接在调用处代码展开),普通函数调用需要保存返回地址,还需要开函数参数栈,这样可以大大减少由函数调用带来的开销
在类内定义(将函数实现的代码直接写在类里面)成员函数,可以不用在函数头部加 inline 关键字,因为编译器会自动查看是否可以内联优化这些函数。
类外定义成员函数,若想定义为内联函数,需用关键字声明
当在类内声明函数,在类外定义函数时,如果想将该函数定义为内联函数,则可以在类内声明时不加 inline 关键字,而在类外定义函数时加上 inline 关键字。可以在声明函数和定义函数的同时加上 inline;也可以只在函数声明时加 inline,而定义函数时不加 inline。只要确保在调用该函数之前把 inline 的信息告知编译器即可。
inline只是声明,是否内联优化取绝于编译器。当程序员定义的 inline 函数包含复杂递归,或者 inlinie 函数本身比较长,编译器一般不会将其展开,而仍然会选择函数调用。即使是普通函数,编译器也可以选择进行优化,将普通函数在“调用”点展开。
内联函数的作用:去除函数只能定义一次的限制。
内联函数可以在头文件中被定义(实现),并被多个 .cpp 文件 include,而不会有重定义错误。这也是设计内联函数的主要目的之一。
内联函数是在编译阶段将函数体嵌入到每一个调用该函数的语句块中,编译器会将程序中出现内联函数的调用表达式用内联函数的函数体来替换。在调用点处直接展开,避免了函数的参数压栈操作,减少了调用的开销。内联函数是真正的函数,在编译时会对参数的类型、函数体内的语句编写是否正确等进行检查。
普通函数是将程序执行转移到被调用函数所存放的内存地址,当函数执行完后,返回到执行此函数前的地方。转移操作需要保护现场,被调函数执行完后,再恢复现场,该过程需要较大的资源开销。将参数压入栈中,将当前运行地址转移到函数体的地址,函数体再从栈中拿出参数运行,最后得出结果返回到原来运行的地址继续运行。
20 new,delete,malloc
malloc :成功申请到内存,返回指向该内存的指针;分配失败,返回 NULL 指针。
new :内存分配成功,返回该对象类型的指针;分配失败,抛出 bad_alloc 异常。
可以使用std::nothrow让new在申请内存失败时也同malloc一样返回NULL指针,而不是抛出std::bad_alloc异常。
A *a = new (std::nothrow) A();
if (a == nullptr) {
// add logs here
return false;
}
A*a = new A[5];
delete []a; //释放A数组空间
对于像int/char/long/int等等简单数据类型,由于对象没有析构函数,所以用delete和delete []是一样的!都不会造成内存泄露! 但通常为了规范起见,new []都配套使用delete []。
但是如果是C++自定义对象数组就不同了!由于delete p只调用了一次析构函数,剩余的对象不会调用析构函数,*所以剩余对象中如果有申请了新的内存或者其他系统资源,那么这部分内存和资源就无法被释放掉了,因此会造成内存泄露或者更严重的问题。(具体原理在内存管理里有提)
new 操作符从自由存储区(allocator个管理的内存块链表—free store)上为对象动态分配内存,而 malloc 函数从堆上动态分配内存。(自由存储区不等于堆)基本上,所有的C++编译器默认用堆来实现自由存储区,也即是缺省的全局运算符new和delete会按照malloc和free的方式来实现,这时由new运算符分配的对象,说它在堆上也对,说它在自由存储区也对。
malloc 的原理:
当开辟的空间小于 128K 时,调用 brk() 函数,通过移动 _edata 来实现;
当开辟空间大于 128K 时,调用 mmap() 函数,通过在虚拟地址空间中开辟一块内存空间来实现。
从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。
1、brk是将数据段(.data)的最高地址指针_edata往高地址推;
2、mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。
这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。brk和mmap在开始时都只是分配内存,实际使用时才会缺页,然后分配物理内存,建立映射。建立物理地址和进程虚拟地址的映射后,进程有可能不会对分配的内存进行操作,所以内核并不会马上引发缺页异常,将物理页面分配给该进程,而只是更新对应的页表项,直到对该页面进行读写访问时,才会进入缺页异常,进行真正的分配。换句话说,linux的思想就是,“实在不行了”再分配。