12
自动类型推导,auto声明变量的类型必须由编译器在编译时期推导而得。
//1.复杂类型变量声明时简化代码
std::vector<int> v_int;
v_int.push_back(101);
// std::vector::iterator iter = v_int.begin();//迭代器类型复杂
auto iter = v_int.begin();//使用auto关键字自动推导类型,简介易读
while(iter != v_int.end())
{
printf("value=%d\n",*iter);
iter++;
}
//2.自适应泛型编程,根据不同的编译环境,或者引用动态库版本升级,导致表达式返回值类型变化,auto可以自适应类型,无需改动代码
auto v_len = strlen("hello world!");//32位返回4字节整型,64位返回8字节整型
printf("v_len=%ld,sizeof(v_len)=%ld\n",v_len,sizeof(v_len));
//3.宏定义的性能提升
//MAX1是传统写法,a和b是表达式时,无论返回a还是b,a或b都会被运算两次
//MAX2中先把a和b计算出来,再进行比较,a和b都只计算了一次
#define MAX1(a, b) ((a) > (b)) ? (a) : (b)
#define MAX2(a, b) ({\
auto _a = (a);\
auto _b = (b);\
(_a > _b) ? _a : _b;})
int num1 = MAX1(1*2*3*4, 5+6+7+8);
int num2 = MAX2(1*2*3*4, 5+6+7+8);
printf("num1=%d\n",num1);
printf("num2=%d\n",num2);
打印
value=101
v_len=12,sizeof(v_len)=8
num1=26
num2=26
和auto的功能类似,decltype用来在编译时期进行自动类型推导,引入decltype是因为auto并不适用于所有的自动类型推导场景。
1、 auto根据=右边的初始值推导出变量的类型,decltype根据exp表达式推导出变量的类型,跟=右边的value没有关系。
2、auto要求变量必须初始化,这是因为auto根据变量的初始值来推导变量类型的,如果不初始化,变量的类型也就无法推导,而decltype不要求。
auto varName=value;
decltype(exp) varName=value;
decltype(exp) varName;//可以不初始化
decltype的推导规则可以简单概述如下:
1、如果exp是一个不被括号()包围的表达式,或者是一个类成员访问表达式,或者是一个单独的变量,decltype(exp)的类型和exp一致。
2、如果exp是函数调用,则decltype(exp)的类型就和函数返回值的类型一致。
3、如果exp是一个左值,或被括号()包围,decltype(exp)的类型就是exp的引用,假设exp的类型为T,则decltype(exp)的类型为T&。
int num2 = 2;
int& func1(int num,char c)//返回值为int&
{
printf("num=%d\n",num);
return num2;
}
int n=0;
const int &r=n;
decltype(n) x=n; //n为Int,x被推导为Int
decltype(r) y=n; //r为const int &,y被推导为const int &
decltype(func1(100,'A')) a=n;//a的类型为int&,func1函数不会真正执行,只是形式
int n2=0,m2=0;
decltype(m2+n2) c=0;//n+m得到一个右值,c的类型为int
decltype(n2=n2+m2) d=c;//n=n+m得到一个左值,d的类型为int &
c = 102;
printf("c=%d,d=%d\n",c,d);//d是c的引用
//左值:表达式执行结束后依然存在的数据,即持久性数据;
//右值是指那些在表达式执行结束不再存在的数据,即临时性数据。
打印
c=102,d=102
C++11使用nullptr取代NULL表示空指针.
NULL缺陷:在VS等编译环境下NULL是宏定义,值为0,也就是0x0000 0000这个内存空间,NULL即表示空指针,又表示0,在某些代码场景存在二意性。
void Func_nul(void* ){
printf("I am void fucntion!\n");
}
void Func_nul(int ){
printf("I am zero fucntion!\n");
}
Func_nul(NULL);//编译错误:error: call to 'Func_nul' is ambiguous
Func_nul(0);
nullptr是nullptr_t类型的右值常量,专门用于初始化空类型的指针。nullptr_t是在C++11新增加的数据类型,nullptr_t是指针空值类型。
nullptr可以被隐式转换成任意的指针类型, 不同类型的指针变量都可以使用nullptr类初始化,编译器会将nullptr隐式转换成int*、char*、double*指针类型。
final 关键字限制某个类不能被继承,或者某个虚函数不能被重写。如果使用 final 修饰函数,只能修饰虚函数,并且要把final关键字放到类或者函数的后面。
final修饰类
被final修饰的类不能作为基类,也就是说不能被其他类所继承。
举例:类A使用final修饰,类B继承于A时报错:error: base ‘A’ is marked ‘final’。
class A final
{
public:
virtual void TestFunc()
{
printf("A Class\n");
}
};
class B :A//error: base 'A' is marked 'final'
{
public:
virtual void TestFunc()
{
printf("B Class\n");
}
};
final修饰函数
被修饰的虚函数禁止了子类对虚函数的重写。
class A
{
public:
virtual void TestFunc() final
{
printf("A Class\n");
}
};
class B:A
{
public:
virtual void TestFunc()//error: declaration of 'TestFunc' overrides a 'final' function
{
printf("B Class\n");
}
};
在成员函数声明或定义中, override 确保该函数为虚函数并覆盖来自基类的虚函数。
override 显示地表明,这个函数是重写基类的虚函数,编译器可以帮助验证 override 对应的方法名是否是基类中所有的,如果没有则报错。
下面例子,类B虚函数TestFunc使用override 修饰,但基类A中并没有这个虚函数,因此报错。
class A
{
public:
};
class B :public A
{
public:
//error: 'TestFunc' marked 'override' but does not override any member functions
//如果不使用override ,则不会编译报错
virtual void TestFunc() override = 0;
};
override带来的好处
1、如果基类中没有该虚函数,则编译会报错。
2、防止在重写基类虚函数时,函数名写错,如果没有override 修饰,函数名写错了编译并不会报错。
C++类有4个特殊成员函数:默认构造函数、析构函数、拷贝构造函数、拷贝赋值运算符。如果程序没有定义这些特殊成员函数,则编译器会隐式的为这个类生成一个默认的特殊成员函数。
这几种特殊成员函数使用default关键字,可以快速生成一个默认成员函数,而不需要写函数体。
default的好处
1、写法简单,节省开发时间。
2、代码执行效率高,当我们使用这个关键字定义的构造函数,在声明变量时,编译器不会去调用构造函数,也不会生成构造函数的代码,高效率提高声明变量的时间。
class School{
public:
School() = default;
virtual ~School() = default;
private:
std::string name;
};
class college : public School{
public:
int age;
~college()
{
printf("~college\n");
}
};
School *pSchool = new college;
delete pSchool;
上面例子,如果School的析构函数不是virtual的,则delete时不会执行college类的析构函数,造成隐式的内存泄漏。
C++11可以使用delete关键字显示地删除默认生成的函数,比如删除一个函数模板的实例,删除类的默认拷贝构造函数等。
template <class T>
T sum(T t1, T t2)
{
return t1 + t2;
}
int sum(int, int) = delete ;//删除函数模板的一个实例
class Student
{
public:
Student() = default;
Student(const Student & c) = delete ;//删除拷贝构造函数
int age;
};
// sum(1,2);//error: call to deleted function 'sum'
float num = sum(1.2, 3.5);
printf("num=%.2f\n",num);//num=4.70
Student s1;
// Student s2 = s1;//error: call to deleted constructor of 'Student'
传统的C++语法中就有引用,C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
1、左值是可以放在赋值号左边可以被赋值的值;左值必须要在内存中有实体;
2、右值当在赋值号右边取出值赋给其他变量的值;右值可以在内存也可以在CPU寄存器。
3、一个对象被用作右值时,使用的是它的内容(值),被当作左值时,使用的是它的地址。
4、左值:指表达式结束后依然存在的持久对象,可以取地址,具名变量或对象 。
5、右值:表达式结束后就不再存在的临时对象,不可以取地址,没有名字。
左值引用和右值引用
左值引用:type &引用名 = 左值表达式;对左值的引用,是给左值取别名。
右值引用:type &&引用名 = 右值表达式;对右值的引用,是给右值取别名。
int a1=10;//a1 是左值
int & a2=a1;//引用左值,是一个左值引用
int&& b2 = 100;//右值引用
printf("a2=%d,b2=%d\n",a2,b2);//a2=10,b2=100
std::move
将一个左值引用强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义。
std::move语句可以将左值变为右值而避免拷贝构造。
std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝。
int a = 1;
std::vector<int> vec;
vec.push_back(std::move(a));
printf("a=%d\n",a);//基本数据类型不存在拷贝构造,a的值不变
std::string str = "Hello";
std::vector<std::string> v;
//调用常规的拷贝构造函数,新建字符数组,拷贝数据,str的值不变
v.push_back(str);
printf("str=%s\n",str.c_str());
//调用移动构造函数,会把原str的数据据为己有,最好不要使用str
v.push_back(std::move(str));
printf("str=%s\n",str.c_str());
auto iter = v.begin();
while(iter != v.end())
{
printf("v.str=%s\n",iter->c_str());
iter++;
}
打印
a=1
str=Hello
str=
v.str=Hello
v.str=Hello
Lambda(匿名函数)表达式是C++11最重要的特性之一,来源于函数式编程的概念。
声明式编程风格:就地匿名定义目标函数或函数对象,有更好的可读性和可维护性。
简洁:不需要额外写一个命名函数或函数对象,,避免了代码膨胀和功能分散。
更加灵活:在需要的时间和地点实现功能闭包。
lambda表达式定义了一个匿名函数,并且可以捕获一定范围内的变量。语法形式如下:
[ capture ] ( params ) opt -> ret { body; };
auto f = [](int a) -> int {return a + 1;};
auto f = [](int a) {return a + 1;};//省略返回值的定义
capture:捕获列表
params:参数列表
opt:函数选项
ret:返回值类型
body:函数体
在实际的使用中,可以省略其返回值的定义(opt -> ret),使用auto自动推导返回值类型。
捕获变量规则
[] 不捕获任何变量
[&]捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)
[=]捕获外部作用域中所有变量,并作为副本在函数体重使用(按值捕获)
[=,&foo] 按值捕获外部作用域中所有变量,并按引用捕获foo变量
[bar] 按值捕获bar变量,同时不捕获其他变量
[this] 捕获当前类中的this指针,让表达式拥有和当前类成员函数同样的访问权限。如果已经使用了&或者=,就默认添加此选项。捕获this的目的是可以在lambda中使用当前类的成员变量和成员函数。
mutable
若( )后面使用mutable修饰,在=传值时,可以在表达式内修改参数,但只是修改了行参,并未修改参数本身的值。
传参示例
class A
{
public:
int i_ = 0;
void func(int x,int y)
{
auto x1 = []{return i_;}; // error,没有捕获外部变量
auto x2 = [=]{return i_ + x + y;}; //ok,按值捕获所有外部变量
auto x3 = [&]{return i_ + x + y;}; //ok,按引用捕获所有外部变量
auto x4 = [this]{return i_;}; //ok,捕获this指针
auto x5 = [this]{return i_ + x + y;}; //error,没有捕获x和y变量
auto x6 = [this,x,y]{return i_ + x + y;}; //ok,捕获了this指针和x、y变量
auto x7 = [this]{return i_++;}; //ok,捕获了this指针,修改成员变量的值
}
};
int a = 0 , b = 0 ;
auto f5 = [a]{return a+b;}; //error,没有捕获b变量
auto f6 = [a,&b]{return a+ (b++);}; //ok,捕获a以及b的引用,对b进行自加
auto f7 = [=,&b]{return a+ (b++);}; //ok, 捕获所有外部变量和b的引用,对b进行自加
实际应用
int a = 1;
int b = 2;
auto fun1 = [=](int c)mutable{
b = a + c;
return b;};
printf("fun1(10)=%d\n",fun1(10));
printf("b=%d\n",b);//值传递,并未修改b的值
std::vector<int> v = {1,2,3,4,5,6,7};
int sum = 0;
for_each(v.begin(),v.end(),[&sum](int val){
sum += val;
});
printf("sum=%d\n",sum);
打印
fun1(10)=11
b=2
sum=28
C++ 11 标准中,为 for 循环添加了一种全新的语法格式,如下所示:
for (declaration : expression){
//循环体
}
declaration:表示此处要定义一个变量,该变量的类型为要遍历序列中存储元素的类型,通常使用auto自动推导变量的类型。
expression:表示要遍历的序列,常见的可以为事先定义好的普通数组或者容器,还可以是用 {} 大括号初始化的序列。
//1.for循环遍历普通数组,尾部\0也会被遍历
char arc[] = "HelloWorld";
for (char ch : arc)
{
if(ch == '\0')
printf(" ");
else
printf("%c",ch);
}
printf(".\n");
//2.for循环遍历 vector 容器
std::vector<char>v1(arc, arc + 5);
for (auto ch : v1)
{
printf("%c",ch);
}
printf(".\n");
//3.遍历用{ }大括号初始化的列表
for (int num : {1, 2, 3, 4, 5})
{
printf("%d",num);
}
printf(".\n");
//4.引用遍历,可修改容器中的值
char arc2[] = "abcde";
std::vector<char>v2(arc2, arc2 + 5);
for (auto &ch : v2)
{
ch++;
}
//for循环遍历输出容器中各个字符
for (auto ch : v2)
{
if(ch == '\0')
printf(" ");
else
printf("%c",ch);
}
printf(".\n");
打印
HelloWorld .
Hello.
12345.
bcdef.
统一初始化,也叫做大括号初始化。就是使用大括号进行初始化的方式。
编译器看到{t1, t2, …, tn}便会做出一个initializer_list,它关联到一个array
//初始化示例
int values[]{ 1, 2, 3 };
std::vector<int> v{ 2, 3, 6, 7 };
std::vector<std::string> cities{
"Berlin", "New York", "London", "Braunschweig"
};
//显示使用std::initializer_list
class P
{
public:
P(int, int){printf("call P::P(int,int)\n");}
P(std::initializer_list<int>){
printf("call P::P(initializer_list)\n");
}
};
P p(77,5); // call P::P(int,int)
P q{77,5}; // call P::P(initializer_list)
P r{77,5,42}; // call P::P(initializer_list)
P s = {77, 5}; // call P::P(initializer_list)
打印
call P::P(int,int)
call P::P(initializer_list)
call P::P(initializer_list)
call P::P(initializer_list)
与assert不同的是,static_assert是在编译期进行检查,而不是在运行期进行检查。static_assert的原理是在编译期检查一个条件是否满足,如果不满足则编译器会报错并输出错误信息。
static_assert通常用于编译期检查一些常量表达式或类型特性,可以帮助程序员在编译期发现一些错误,提高程序的健壮性和可维护性。
static_assert(expression, message);
其中,expression是一个常量表达式,用于检查某个条件是否满足;message是一个字符串,用于描述错误信息。
template <int N>
struct check_num
{
static_assert(N > 0, "N must be greater than 0"); // 检查N是否大于0
static const bool value = (N % 8) == 0; // 检查N是否是8的整数倍
};
int arr[8];//比如是7,则编译不通过
// 检测arr占用字节数是否是8的整数倍
static_assert(check_num<sizeof(arr)>::value, "The array size must be an integer multiple of 8");
printf("Array size = %ld\n",sizeof(arr));
把函数返回值写的(参数)的后面,下面例子,auto是占位符,->后面才是返回值类型。
auto Fun(int a, int b) ->int//等同于下面的写法
//int Fun(int a, int b)
{
return a + b;
}
int sum = Fun(1,2);
printf("sum=%d\n",sum);//sum=3
用法一:返回一个函数指针类型
1、使用传统函数声明语法无法将函数指针类型作为返回类型直接使用,所以需要使用typedef给函数指针类型创建别名 bar,再使用别名作为函数的返回类型。
2、使用函数返回类型后置语法则没有这个问题。
int bar_impl(int x)
{
return x*2;
}
typedef int(*bar)(int);
//传统前置返回值,需要用typedef定义别名
bar foo1()
{
return bar_impl;
}
//使用后置返回值则不需要定义别名,直接使用即可
auto foo2()->int(*)(int)
{
return bar_impl;
}
auto func = foo2();
printf("func(16)=%d\n",func(16));//func(16)=32
用法二:推导函数模板返回类型
配合decltype说明符,自动推导函数模板返回值类型。
template<class T1, class T2>
auto sum(T1 t1, T2 t2)->decltype(t1 + t2)
{
return t1 + t2;
}
auto num = sum(4, 2);
printf("num=%d\n",num);//num=6
枚举起源于C,所以C++的枚举直接从C语言继承过来。
C枚举不足
1、由于C语言是没有命名空间的,所以枚举的成员在C++中的作用域也是全局的。
2、由于枚举本质是常量数值,所以枚举成员会被隐式转换为整型。这提供了一些便捷性同样也带来了一定的风险。
3、枚举为int类型4字节,当超出时会溢出。(和操作系统相关,部分操作系统当超出4字节int范围时,枚举会自动扩容为8字节)。
C++枚举类
为了解决上述问题,C++11引入了枚举类(enum class)也叫强类型枚举(strong-typed enum)。
而枚举类的声明:在enum 的后面添加class 关键字。
优点:
强作用域:枚举类的成员会严格按照作用域空间。
隐式转换限制:枚举类的成员不可以和整型进行转换(可以强制类型转换)。
指定底层类型:枚举类默认的底层类型是int,还支持显式的指定底层类型,语法: enum_name : type。需要注意的是type是处理wchar_t(宽字符)之外的所有整型类型。
//原始枚举,从C语言继承,枚举值是全局变量
enum Enum
{
A = 10, //10
B, //11
C = 10, //10
D, //11
//如果枚举整型大于int,枚举类型自动由int4字节升级为long int8字节,ubuntu20.04环境
};
enum class Enum1: long long int//指定枚举数据类型
{
//强作用域,如果Enum1是普通枚举,则下面的枚举和Enum是重复定义
A = 4, //4
B = 0, //0
C , //1
D , //2
};
enum class Enum2: long long int
{
//和Enum1不会重复定义
A = 5, //5
B = 11, //11
C = LONG_MAX, //9223372036854775807
D = LLONG_MAX, //9223372036854775807
};
printf("sizeof(int)=%ld\n",sizeof(int));
printf("sizeof(long int)=%ld\n",sizeof(long int));
printf("sizeof(long long int)=%ld\n",sizeof(long long int));
printf("sizeof(Enum::A)=%ld\n" ,sizeof(A));//4字节,如果定义了E = LONG_MAX,则是8字节
printf("sizeof(Enum1::A)=%ld\n" ,sizeof(Enum1::A));
printf("Enum::A=%d\n",A);//no warning
printf("Enum2::A=%lld\n",Enum2::A);//warning: format ‘%lld’ expects argument of type ‘long long int’, but argument 2 has type ‘Enum2’ [-Wformat=]
//原枚举,支持和整型的隐式转换
int num = A;
//枚举类,不支持和整型的隐式转换
// long long int num1 = Enum1::A;//error: cannot initialize a variable of type 'long long' with an rvalue of type 'Enum1'
long long int num1 = (long long int)Enum1::A;//强制类型转换,ok
printf("num=%d\n",num);
printf("num1=%lld\n",num1);
打印
sizeof(int)=4
sizeof(long int)=8
sizeof(long long int)=8
sizeof(Enum::A)=4
sizeof(Enum1::A)=8
Enum::A=10
Enum2::A=5
num=10
num1=4
C++开发中通常会把类、结构体、枚举定义在.h头文件中,有时会出现A.h包含了B.h,B.h也包含了A.h,交叉包含的问题。
使用前置声明的好处
1、防止头文件相互交叉包含。
2、C++中头文件不会单独编译,在cpp文件编译时会同时编译依赖的头文件,不使用前置声明会添加多余的头文件依赖,产生额外的编译开销。
3、一句话:尽量不要把包含头文件写在.h头文件,而是写在cpp文件。
在mainwindow.h中定义了结构体、类、枚举类,C++11支持枚举类前置声明。
struct Country{
int area;
std::string name;
};
class Student
{
public:
int age;
std::string name;
};
enum class Enum1: long long int//指定枚举数据类型
{
A = 4, //4
B = 0, //0
C , //1
D , //2
};
TestEnum.h
#ifndef TESTENUM_H
#define TESTENUM_H
//前置声明
class Student;
struct Country;
enum class Enum1: long long int;
class TestEnum
{
public:
TestEnum();
private:
Enum1 m_Enum1;
Student *pStu;
Country *pCountry;
};
#endif // TESTENUM_H
TestEnum.cpp
#include "TestEnum.h"
#include "mainwindow.h"
TestEnum::TestEnum()
{
m_Enum1 = Enum1::A;
pStu = new Student;
pStu->age = 10;
pCountry = new Country;
pCountry->area = 1000;
printf("m_Enum1=%lld\n",m_Enum1);
printf("pStu->age=%d\n",pStu->age);
printf("pCountry->area=%d\n",pCountry->area);
}
在头文件使用前置声明,在cpp文件保护依赖头文件。
打印
m_Enum1=4
pStu->age=10
pCountry->area=1000
在namespace加inline关键字即可。
内联命名空间的特点时,不需要使用using语句,也不需使用命名空间前缀,就可以直接在外层命名空间使用该命名空间内部的内容。
当然既不使用using,也不使用命名空间前缀,则不同的内联命名空间不能定义相同的内容(比如同名类)。
内联命名空间带来的好处
1、省事,省的写using和命名空间前缀就可以直接使用空间里的内容。
2、在库开发中,库的版本迭代升级提供新的接口,同时保留旧的接口,就可以使用内联命名空间,为库的调用者提供便利。
//老接口
namespace inline_ns1{
class AA{
public:
void testFunc()
{
printf("old class AA\n");
}
};
}
//新接口
inline namespace inline_ns2{
class AA{
public:
void testFunc()
{
printf("class AA\n");
}
};
}
//新接口,不使用命名空间前缀
AA aa;
aa.testFunc();
//如果想使用老接口,加上命名空间前缀即可
inline_ns1::AA aa2;
aa2.testFunc();
打印
class AA
old class AA
#include
#include
#define ONE_PARM(parm1) printf("first=%-2c\n",(parm1))
#define TWO_PARM(parm1, parm2) printf("first=%-2c,second=%-5d\n",(parm1),(parm2))
#define THREE_PARM(parm1, parm2, parm3) printf("first=%-2c,second=%-5d,third=%-20s\n",(parm1),(parm2),(parm3))
#define GET_PARM(_1,_2,_3,func,...) func
#define PRINTF(...) GET_PARM(__VA_ARGS__, THREE_PARM, TWO_PARM, ONE_PARM,...)(__VA_ARGS__)
int num=1;
char ch='A';
std::string str = "string";
PRINTF(ch);
PRINTF(ch,num);
PRINTF(ch,num,str.c_str());
打印
first=A
first=A ,second=1
first=A ,second=1 ,third=string