小编是双非本科大一菜鸟不赘述,欢迎米娜桑来指点江山哦(QQ:1319365055)
非科班转码社区诚邀您入驻
小伙伴们,打码路上一路向北,彼岸之前皆是疾苦
一个人的单打独斗不如一群人的砥砺前行
这是我和梦想合伙人组建的社区,诚邀各位有志之士的加入!!
社区用户好文均加精(“标兵”文章字数2000+加精,“达人”文章字数1500+加精)
直达: 社区链接点我
___
众所周知 C++ 是一个历来小心谨慎,不敢尝试冒进的 “刻板” 语言,C++ 标准委员会按常理都会一段时间对 C++ 进行更新修正。广为人知的是 C++ 上一个版本 C++98 ,本来说十年磨一剑,在 18 年会搞出 C++18 ,但是很明显标准委员会摸鱼的摸鱼,犹豫的犹豫,计划一拖再拖最后问世于 2011 年,索性得名——C++11。
相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。
相比较而言,C++11能更好地用于系统开发和库开发、语言更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多。但是由于 C++ 的保守不敢向 python 一样激进,到现在都没有一个网络库,这也是 C++ 一直以来的一大遗憾。
接下来就来看看 C++11 有哪些亮眼的语法吧
C++98 允许使用大括号 {} 对数组或者结构体元素进行统一的列表初始值设定,举个栗子:
struct Num
{
int _x;
int _y;
};
int main()
{
//数组元素初始化
int a[] = { 1, 2, 3};
int b[3] = { 1 };
//结构体元素初始化
Num n = { 1, 2 };
return 0;
}
众所周知,{} 的内容就是初始化列表,C++11 拓宽了 {} 业务范围,所有的内置类型和自义定类型都可以使用初始化列表初始化,在 {} 之后可以使用 = ,也可以不使用,但是在进行 new 表达式初始化时一定不能加 = (C++11新增语法):
struct Num
{
int _x;
int _y;
};
int main()
{
//{}对内置类型初始化
int x1 = { 1 }; //等号
int x2{ 2 }; //不加等号
//{}对数组元素进行初始化
int a[]{1, 2, 3}; //等号
int b[3]{1}; //不加等号
//{}对结构体元素进行初始化
Num p{ 1, 2 }; //不加等号
//C++11中列表初始化也可用于new表达式中(C++98无法初始化)
int* p1 = new int[3]{0};
int* p2 = new int[3]{1,2,3};
return 0;
}
同样的,构造函数初始化也可以这样:
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//构造函数的一般调用
Date d1(2022, 10, 07);
//C++11支持列表初始化这里调用构造函数初始化
Date d2 = { 2022, 10, 08 }; //等号
Date d3{ 2022, 10, 09 }; //不加等号
return 0;
}
initializer_list 是一个容器,是 C++11 新增的,但是他不像其他容器一样提供了花里胡哨的成员函数
提供了 begin 和 end 函数,用于迭代器遍历;以及获取容器中的元素个数的 size 函数。
initializer_list 本质就是一个大括号括起来的列表,如果用auto关键字定义一个变量来接收一个大括号括起来的列表,然后以 t y p e i d ( 变量名 ) . n a m e ( ) \color{red} {typeid(变量名).name()} typeid(变量名).name() 的方式查看该变量的类型,此时会发现该变量的类型就是 initializer_list
int main()
{
auto num = { 1, 2, 3 };
cout << typeid(num).name() << endl; //class std::initializer_list
return 0;
}
思考一下为什么 initializer_list 没有提供相应的增删查改的接口函数,其实内涵就是告诉你咱们 initializer_list 的目的不是去存储数据的,而是为了让其他容器支持列表初始化:
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//用大括号括列表对容器初始化
vector<int> v = { 1, 2, 3, 4, 5 };
list<int> l = { 10, 20, 30, 40, 50 };
vector<Date> vd = { Date(2022, 8, 29), Date{ 2022, 8, 30 }, { 2022, 8, 31 } };
map<string, string> m{ make_pair("sort", "排序"), { "insert", "插入" } };
//用大括号列表对容器赋值
v = { 5, 4, 3, 2, 1 };
return 0;
}
C++98并不支持直接用列表对容器进行初始化,这种初始化方式是在C++11引入initializer_list后才支持的,而这些容器之所以支持使用列表进行初始化:
根本原因是因为 C + + 11 给这些容器增加了一个构造函数 \color{red} {根本原因是因为 C++11 给这些容器增加了一个构造函数} 根本原因是因为C++11给这些容器增加了一个构造函数
,这个构造函数是以 i n i t i a l i z e r l i s t 作为参数的 \color{red} {,这个构造函数是以 initializer_list 作为参数的} ,这个构造函数是以initializerlist作为参数的
当用列表对容器进行初始化时,这个列表被识别成 initializer_list 类型,于是就会调用这个新增的构造函数对该容器进行初始化。这个新增的构造函数要做的就是遍历 initializer_list 中的元素,然后将这些元素依次插入到要初始化的容器当中即可。
namespace cl
{
template<class T>
class vector
{
public:
typedef T* iterator;
vector(initializer_list<T> il)
{
_start = new T[il.size()];
_finish = _start;
_endofstorage = _start + il.size();
//迭代器遍历
//typename initializer_list::iterator it = il.begin();
//while (it != il.end())
//{
// push_back(*it);
// it++;
//}
//范围for遍历
for (auto e : il)
{
push_back(e);
}
}
vector<T>& operator=(initializer_list<T> il)
{
vector<T> tmp(il);
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_endofstorage, tmp._endofstorage);
return *this;
}
private:
iterator _start;
iterator _finish;
iterator _endofstorage;
};
}
使用迭代器方式遍历时,需要在迭代器类型前面加上typename关键字,指明这是一个类型名字。因为这个迭代器类型定义在一个类模板中,在该类模板未被实例化之前编译器是无法识别这个类型的。最好也增加一个以 initializer_list 作为参数的赋值运算符重载函数,以支持直接用列表对容器对象进行赋值,但实际也可以不增加,下面代码依然可以正常执行:
vector<int> v = { 1, 2, 3 };
v = { 3, 2, 1 };
对于第一行代码,就是调用以initializer_list作为参数的构造函数完成对象的初始化。
而对于第二行代码,会先调用initializer_list作为参数的构造函数构造出一个vector对象,然后再调用vector原有的赋值运算符重载函数完成两个vector对象之间的赋值。
C++11 提供了许多简化声明的方法,比较突出的就是 a u t o \color{red} {auto} auto 。
auto 是一个存储类型的说明符,表示该变量为自动存储类型,auto 在局域里就没有价值了,因为局域内的局部变量默认都是 auto 类型,C++11中废弃auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。比如:
int main()
{
int i = 10;
auto p = &i;
auto pf = strcpy;
cout << typeid(p).name() << endl; //int *
cout << typeid(pf).name() << endl; //char * (__cdecl*)(char *,char const *)
map<string, string> dict = { { "sort", "排序" }, { "insert", "插入" } };
//map::iterator it = dict.begin();
auto it = dict.begin(); //简化代码
return 0;
}
自动类型推断在某些场景下是非常必要的,因为编译器要求定义变量必须先给出变量实际类型,而如果自己设定的类型可能会出问题。比如:
int main()
{
short a = 32670;
short b = 32670;
//c如果给成short,会造成数据丢失,如果能够让编译器根据a+b的结果推导c的实际类型,就不会存在问题
short c = a + b;
return 0;
}
还有一个与 auto 相似的关键字,名为 decltype ,他可以将变量的类型声明为表达式指定的类型, 那为什么不用 typeid(变量名).name() 来获取类型呢?通过 t y p e i d ( 变量名 ) . n a m e ( ) \color{red} {typeid(变量名).name()} typeid(变量名).name() 的方式确实可以获取一个变量的类型,但无法用获取到的这个类型去定义变量。
decltype 除了能推演表达式类型,还能推演函数返回值类型,指定函数返回值类型。比如:
//decltype 推演表达式类型
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
decltype(t1*t2) ret;
cout << typeid(ret).name() << endl;
}
int main()
{
const int x = 1;
double y = 2.2;
decltype(x*y) ret;
decltype(&x) p;
cout << typeid(ret).name() << endl; //double
cout << typeid(p).name() << endl; //int const *
F(1, 'a'); //int
F(1, 2.2); //double
return 0;
}
//decltype 推演函数返回值类型
void* GetMemory(size_t size)
{
return malloc(size);
}
int main()
{
//如果没带参数,推导函数的类型
cout << typeid(decltype(GetMemory)).name() << endl;
//带参数列表,推导函数返回值类型,
//注意:此处只是推演,不会执行函数
cout << typeid(decltype(GetMemory(0))).name() << endl;
return 0;
}
decltype 指定函数返回值类型
template<class T1, class T2>
auto Add(T1 t1, T2 t2)->decltype(t1+t2)
{
decltype(t1+t2) ret;
ret = t1 + t2;
cout << typeid(ret).name() << endl;
return ret;
}
int main()
{
cout << Add(1, 2) << endl;; //int
cout << Add(1, 2.2) << endl;; //double
return 0;
}
C++98 里,一共有 6 个默认成员函数:构造函数,析构函数,拷贝构造函数,拷贝赋值函数,取地址重载函数,const取地址重载函数(前四个成员函数最重要,后面两个成员函数一般不会用到)
到了 C++11 ,新增了 2 个默认成员函数: 移动构造函数和移动赋值重载函数 \color{red} {移动构造函数和移动赋值重载函数} 移动构造函数和移动赋值重载函数
移动构造函数的生成条件:没有自己实现移动构造函数,并且没有自己实现析构函数、拷贝构造函数和拷贝赋值函数。移动赋值重载函数同理。但是麻烦的事情就是他和 C++98 的不同,并不是单纯的没有实现移动构造和移动赋值编译器就会默认生成,所以导致如果我们实现了移动构造/移动赋值,就算没有实现拷贝构造和拷贝赋值,编译器也不会生成默认的拷贝构造和拷贝赋值。
他们对于内置类型会完成值拷贝(浅拷贝),对于自定义类型成员,如果成员实现了移动构造就调用它的移动构造,否则调用它的拷贝构造。我们可以用一个简化的 string 类来验证一下:
namespace exam
{
class string
{
public:
//构造函数
string(const char* str = "")
{
_size = strlen(str); //初始时,字符串大小设置为字符串长度
_capacity = _size; //初始时,字符串容量设置为字符串长度
_str = new char[_capacity + 1]; //为存储字符串开辟空间(多开一个用于存放'\0')
strcpy(_str, str); //将C字符串拷贝到已开好的空间
}
//交换两个对象的数据
void swap(string& s)
{
//标准库swap
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
//拷贝构造函数(现代写法)
string(const string& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str); //构造函数构造出一个C字符串对象
swap(tmp); //交换这两个对象
}
//移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
//拷贝赋值函数(现代写法)
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 深拷贝" << endl;
string tmp(s); //s拷贝构造出对象tmp
swap(tmp); //交换这两个对象
return *this;
}
//移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
//析构函数
~string()
{
delete[] _str;
_str = nullptr; //置空,防止非法访问
_size = 0;
_capacity = 0;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
还需要一个 person 类,成员 name 就是我们实现的 string
class Person
{
public:
//构造函数
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
//拷贝构造函数
Person(const Person& p)
:_name(p._name)
, _age(p._age)
{}
//拷贝赋值函数
Person& operator=(const Person& p)
{
if (this != &p)
{
_name = p._name;
_age = p._age;
}
return *this;
}
//析构函数
~Person()
{}
private:
exam::string _name;
int _age;
};
int main()
{
Person s1("嘉然", 19);
Person s2 = std::move(s1); //想调用默认生成的移动构造,move赋予了右值属性
return 0;
}
虽然 Person 类当中没有实现移动构造和移动赋值,但拷贝构造、拷贝赋值和析构函数都实现了,因此不会生成默认的移动构造和移动赋值
代码中用右值去构造 s2 对象,但由于 Person 类没有生成默认移动构造函数,因此这里会调用 Person 拷贝构造函数(拷贝构造既能接收左值也能接收右值),这时在Person的拷贝构造函数中就会调用string的拷贝构造函数对name成员进行深拷贝。
如果要让 Person 类生成默认的移动构造函数,就必须将 Person 类中的拷贝构造、拷贝赋值和析构函数全部注释掉,这时用右值去构造 s2 对象时就会调用 Person 默认生成的移动构造函数。
Person 默认生成的移动构造,对于内置类型成员 age 会进行值拷贝,而对于自定义类型成员 name 会深拷贝,因为我们的 string 类实现了移动构造函数,因此它会调用 string 的移动构造函数进行资源转移。
如果我们将 string 类当中的移动构造函数注释掉,那么 Person 默认生成的移动构造函数,就会调用 string 类中的拷贝构造函数对 name 成员进行深拷贝
默认生成的构造函数,对于自定义类型成员会调用其构造函数进行初始化,但不会对内置类型成员进行处理。于是 C++11 支持非静态成员变量在声明时进行初始化赋值,默认生成的构造函数会使用这些缺省值对成员进行初始化, 注意这里不是直接初始化,只是给一个缺省值! \color{red} {注意这里不是直接初始化,只是给一个缺省值!} 注意这里不是直接初始化,只是给一个缺省值!
class Person
{
public:
//...
private:
//非静态成员变量,可以在成员声明时给缺省值
cl::string _name = "张三";
int _age = 20;
static int _n; //静态成员变量不能给缺省值
};
delete 在 C++98 中全是用于对资源的释放删除,在 C++11 中 delete 有了新的功能他可以禁止生成默认函数,比如如果想要实现一个不能被继承的类,我就有两种方法:
- 可以将该函数设置成私有,并且只用声明不用定义,这样当外部调用该函数时就会报错
- 可以在该函数声明后面加上=delete,表示让编译器不生成该函数的默认版本,我们将=delete修饰的函数称为删除函数
或者要让一个类不能被拷贝,可以用 =delete 修饰他的拷贝构造和拷贝赋值。
class CopyBan
{
public:
CopyBan()
{}
private:
CopyBan(const CopyBan&) = delete;
CopyBan& operator=(const CopyBan&) = delete;
};
被final修饰的类叫做最终类,最终类无法被继承
class cl final //被final修饰,该类不能再被继承
{
//...
};
final修饰虚函数,表示该虚函数不能再被重写,如果子类继承后重写了该虚函数则编译报错
class Person
{
public:
virtual void Print() final //final修饰虚函数不能再被重写
{
cout << "Person" << endl;
}
};
//子类
class Student : public Person
{
public:
virtual void Print() //重写编译报错
{
cout << "Student" << endl;
}
};
override修饰子类的虚函数,检查子类是否重写了父类的某个虚函数,如果没有没有重写则编译报错
class Person
{
public:
virtual void Print()
{
cout << "Person" << endl;
}
};
//子类
class Student : public Person
{
public:
virtual void Print() override //检查子类是否重写父类的某个虚函数
{
cout << "Student" << endl;
}
};
aqa 芭蕾 eqe 亏内,代表着开心代表着快乐,ok 了家人们