C++11是C++编程语言的一个版本,于2011年发布。C++11引入了许多新特性,为C++语言提供了更强大和更现代化的编程能力。这篇文章将对C++11的一些新增特性进行讲解和实际应用场景。
统一的列表初始化
{}初始化
在C++98中,使用{}符号的一般只仅限于对数组和结构体元素的初始化;
在C++11中,引入了使用大括号{}进行初始化的列表初始化(又称为统一初始化语法)。这种初始化方式用于各种情况下的对象初始化,包括变量、数组、结构体、类等。
cpp
struct Point
{
int _x;
int _y;
};
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()
{
//C++98就支持的初始化(数组和结构体)
int arr1[] = { 1,2,3,4 };
int arr2[]{ 1,2,3,4 };
Point p{ 1,2 };
//old
Date d1(2024, 03, 21);
//new
Date d2 = { 2024,03,21 };
Date d3{ 2024,03,21 };//使用初始化列表时,加不加等于号'='没有区别
}
使用{}进行初始化有以下几个优点:
-
统一性:无论是内置类型还是自定义类型,都可以使用同样的方式进行初始化,提高了代码的一致性和可读性。
-
防止窄化转换(narrowing conversion):在{}初始化中,如果存在可能导致数据丢失的窄化转换(比如将浮点数转换为整数),编译器会报错,避免了潜在的问题。
-
初始化列表的支持:{}初始化还可以用于初始化列表,可以方便地一次性初始化多个元素,比如数组或结构体的成员。
std::initializer_list
std::initializer_list是C++11引入的一种特殊的容器,用于表示初始化列表 。它允许以大括号{}的形式初始化一组值,并将其作为参数传递给函数或构造函数。
使用std::initializer_list,可以方便地传递不确定数量的参数,并以统一的方式进行处理。它的语法类似于标准库容器(如vector、list等),可以使用迭代器等操作进行访问和处理。
下面是一个简单的示例,展示了如何使用std::initializer_list:
cpp
void printValues(std::initializer_list<int> values) {
for (auto value : values) {
cout << value << " ";
}
cout << endl;
}
int main() {
printValues({1, 2, 3, 4, 5}); // 使用初始化列表调用函数
return 0;
}
在上述示例中,printValues函数接受一个std::initializer_list类型的参数,并使用for循环打印每个值。在main函数中,我们使用初始化列表{1, 2, 3, 4, 5}调用了printValues函数。
decltype
decltype是C++11引入的一个关键字,用于获取表达式的类型。它可以在编译时推导出表达式的类型,并将其作为decltype表达式的返回值。
cpp
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;
const double y = 2.2;
decltype(x*y) ret; // ret的类型是double
decltype(&x) p; // p的类型是int*
cout << typeid(ret).name() << endl;
cout << typeid(p).name() << endl;
F(1, 'a');
return 0;
}
右值引用
什么是左值和左值引用?
左值是一个表示数据的表达式,我们可以获取它的地址,左值由此定义出现在赋值号的左边;
左值引用就是对左值的引用。
cpp
int main()
{
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
return 0;
}
什么是右值和右值引用
右值也是一个数据的表达式:如字面常量、表达式返回值、函数返回值等;
右值可以出现在赋值符号的右边,不能出现在左边。右值不可以取地址。
右值引用就是对右值的引用。
cpp
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
// 这里编译会报错:error C2106: "=": 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}
左值总结
cpp
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a; // ra为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
右值总结
cpp
int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: "初始化": 无法从"int"转换为"int &&"
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
return 0;
}
应用场景
cpp
namespace fnc
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
//string operator+=(char ch)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
}
void func1(fnc::string s)
{}
void func2(const fnc::string& s)
{}
int main()
{
fnc::string s1("hello world");
// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
func1(s1);
func2(s1);
string operator+=(char ch) 传值返回存在深拷贝
string& operator+=(char ch) 传左值引用没有拷贝提高了效率
s1 += '!';
fnc::string ret1 = fnc::to_string(1234);//默认构造+拷贝构造+拷贝构造->默认构造
fnc::string ret2;//默认构造
ret2 = fnc::to_string(1234);//默认构造+拷贝构造+赋值构造->默认构造+移动赋值
fnc::string ret3(move(ret2));//将ret2强制转换为右值,这样就使用了移动构造,但这样会导致ret2是空的了;
for (auto a : ret2)
{
cout << a << endl;
}
return 0;
}
链表移动构造的应用场景
cpp
int main()
{
fnc::list<fnc::string> lt;
fnc::string s1("1111");
lt.push_back(s1);
cout << "-----" << endl;
lt.push_back(fnc::string("2222"));
cout << "-----" << endl;
lt.push_back("2222");
cout << "-----" << endl;
return 0;
}
完美转发
完美转发是一种编程技巧,主要用于在泛型函数中保留参数的类型和值类别(左值或右值),以便更高效准确地传递参数。通过使用右值引用和模板类型推导,完美转发允许我们在函数中以原始参数的形式将参数传递给其他函数,避免不必要的拷贝操作,提高性能。
为了实现完美转发,通常会使用std::forward函数。std::forward被称为完美转发的原因是它能保持原始参数的值属性不变。也就是说,如果原始参数是左值,经过std::forward处理后该参数还是左值;如果原始参数是右值,经过std::forward处理后它还是右值。
在C++11中,完美转发与万能引用(Universal Reference)紧密相关。使用T&&类型的形参既可以绑定右值,又可以绑定左值。但需要注意的是,只有在发生类型推导时,T&&才表示万能引用;否则表示右值引用。
cpp
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
//Fun(t);
Fun(std::forward<T>(t));
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
新的类功能
cpp
//移动构造函数
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p)
:_name(p._name),
_age(p._age)
{
cout << "Person(const Person& p):拷贝构造" << endl;
}
/*Person& operator=(const Person& p)
{
if (this != &p)
{
_name = p._name;
_age = p._age;
}
cout << "Person& operator=(const Person& p) 赋值重载" << endl;
return *this;
}*/
/*~Person()
{}*/
private:
fnc::string _name;
int _age;
};
int main()
{
Person s1("haha",1);
cout << "-----" << endl;
Person s2(s1);//拷贝构造
cout << "-----" << endl;
Person s3;
s3 = s1;//赋值
cout << "-----" << endl;
Person s4=move(s1);//移动构造
cout << "-----" << endl;
Person s5;
s5 = move(s2);
}
default
C++中的default是一个关键字,它在类特殊成员函数中的作用是指示编译器生成默认的实现。
在C++11标准之前,当程序员不显示定义类的构造函数、析构函数、拷贝构造函数、拷贝赋值运算符等特殊成员函数时,编译器会自动为其生成一个默认的实现。而在C++11标准引入了default关键字后,程序员可以使用"default"来显式地指示编译器生成默认的实现,避免显式的定义空实现。
比如,在以下情况下可以使用"default":
- 默认构造函数:如果类没有定义任何构造函数,可以使用"default"来指示编译器生成默认的构造函数。
cpp
class MyClass {
public:
MyClass() = default;
};
- 默认析构函数:如果类不需要进行特殊的资源管理,可以使用"default"来指示编译器生成默认的析构函数。
cpp
class MyClass {
public:
~MyClass() = default;
};
- 默认拷贝构造函数和拷贝赋值运算符:当类的成员变量都可以按照默认的方式进行拷贝,可以使用"default"来指示编译器生成默认的拷贝构造函数和拷贝赋值运算符。
cpp
class MyClass {
public:
MyClass(const MyClass&) = default;
MyClass& operator=(const MyClass&) = default;
};
需要注意的是,使用"default"关键字生成默认函数时,编译器会根据类的成员变量来决定是否生成合适的实现。如果类中存在不能被默认实现的成员变量,那么编译器将无法生成默认函数,此时需要显式地定义相应的特殊成员函数。
delete
在C++中,delete是一个关键字,用于删除特殊成员函数或阻止某些函数的使用。
- 删除特殊成员函数:通过在类定义中声明特殊成员函数,并在后面加上
= delete;
来删除该函数的默认生成。例如:
cpp
class MyClass {
public:
MyClass() = delete; // 删除默认构造函数
MyClass(const MyClass&) = delete; // 删除拷贝构造函数
MyClass& operator=(const MyClass&) = delete; // 删除拷贝赋值运算符
};
这样一来,当程序尝试使用删除的函数时,编译器将会报错。
- 阻止函数的使用:通过在函数声明后面加上
= delete;
表示删除该函数的定义,阻止程序使用该函数。例如:
cpp
void foo(int) = delete; // 阻止使用int型参数的foo函数
void bar() = delete; // 阻止使用无参数的bar函数
这样一来,如果程序尝试调用被删除的函数,编译器将会报错。
通过使用delete关键字,可以更好地控制特殊成员函数的生成和禁止某些函数的使用,提高代码的可读性和安全性。