C++11小语法与拷贝问题
- auto关键字
- 范围for
- initializer_list
- 深拷贝与浅拷贝
- 写时拷贝
以下代码环境为 VS2022 C++。
auto关键字
在早期 C/C++ 中 auto 的含义是:使用 auto 修饰的变量,是具有自动存储器的局部变量,不过一般都会隐藏,导致后来不重要了。C++11中,标准委员会赋予了 auto 全新的含义,即:auto 不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto 声明的变量必须由编译器在编译时期推导而得。
-
用 auto 声明指针类型时,用 auto 和 auto* 没有任何区别。
-
用 auto 声明引用类型时则必须加 &。
-
当在同一行用 auto 声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
-
auto 不能作为函数的参数,但可以做返回值,不过建议谨慎使用。
-
auto 不能直接用来声明数组。
#include <iostream>
void test1()
{
// auto 声明的变量必须初始化,否则报错
//auto one;
// 以下变量类型由编译器在编译时期推导而得
auto a = 10;
auto b = 'C';
auto c = 1.5;
// 1.用 auto 声明指针类型,auto 和 auto* 两者没有区别
auto d = &a;
auto* e = &a;
// 2.auto 声明引用类型需要加上 &
auto& f = a;
// 3.同一行用 auto 声明多个变量,必须是相同的类型,不然报错
//auto g = 1, h = 'c', i = 2.5;
auto j = 1, k = 2, l = 3;
// 5.auto不能直接用来声明数组,但可声明指针去修改
//auto m[10] = { 0 };
//auto n[] = { 1, 2, 3 };
int arr[] = { 1, 2, 3 };
auto o = arr;
o[0] = 5;
o[1] = 6;
}
// 4.auto 不能作为函数的参数,
//void test2(auto number)
//{
// ;
//}
// 但可以做返回值,不过建议谨慎使用
auto test3()
{
return 0;
}
int main()
{
test1();
return 0;
}
范围for
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此 C++11 中引入了基于范围的 for 循环。for 循环后的括号由冒号 “ : ” 分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束。
范围 for 可以作用到数组和容器对象上进行遍历,范围 for 的底层很简单,容器遍历实际就是替换为迭代器,这个从汇编层也可以看到。
#include <iostream>
#include <vector>
#include <list>
void test1()
{
int arr1[] = { 1, 2, 3, 4, 5 };
std::vector<int> arr2 = { 5, 4, 3, 2, 1 };
std::list<int> arr3 = { 1, 3, 5, 7, 9 };
// 一般遍历
int len1 = sizeof(arr1) / sizeof(arr1[0]);
for (int i = 0; i < len1; ++i)
{
std::cout << arr1[i] << " ";
}
std::cout << std::endl;
std::vector<int>::iterator it1 = arr2.begin();
while (it1 != arr2.end())
{
std::cout << *it1 << " ";
++it1;
}
std::cout << std::endl;
std::list<int>::iterator it2 = arr3.begin();
while (it2 != arr3.end())
{
std::cout << *it2 << " ";
++it2;
}
std::cout << std::endl;
std::cout << "---------" << std::endl;
// 范围for本质是替换为迭代器遍历,
// 只要能用迭代器遍历的容器都能使用范围for
for (const auto& e : arr1)
{
std::cout << e << " ";
}
std::cout << std::endl;
for (const auto& e : arr2)
{
std::cout << e << " ";
}
std::cout << std::endl;
for (const auto& e : arr3)
{
std::cout << e << " ";
}
std::cout << std::endl;
}
int main()
{
test1();
return 0;
}
initializer_list
为了使 C++ STL 中的容器支持与数组一样的使用 " { 数据1, 数据2… } " 初始化,在 C++11 引入了 initializer_list 类。
// 数组使用 { } 初始化
int arr1[5] = { 1, 2, 3, 4, 5 };
int arr2[] = { 5, 4, 3, 2, 1 };
详细可参考:std::initializer_list
" { } " 内的数据是在栈上开辟存储的,而 initializer_list 类里封装有两个指针,一个指向 " { } " 第一个元素,另一个指向最后一个元素下一个位置,在 VS2022 调试时可以看到:
并且其中所有元素都加上了 const,表示 " 只读 ",无法修改。
因此使用 C++ STL 的容器初始化时,在 initializer_list 类的基础上可以使用 { } 初始化:
#include <iostream>
#include <vector>
#include <deque>
#include <list>
#include <map>
#include <set>
void test1()
{
std::vector<int> arr1 = { 1, 2, 3, 4, 5 };
std::list<int> arr2 = { 1, 2, 3, 4, 5 };
std::string arr3 = { 'a', 'b', 'c', 'd', '\0' };
std::deque<int> arr4 = { 1, 2, 3, 4, 5 };
std::map<int, char> arr5 = {{1, 'a'}, {2, 'b'}, {3, 'c'}, {4, 'd'}, {5, 'e'}};
std::set<int> arr6 = { 1, 2, 3, 4, 5 };
}
int main()
{
test1();
return 0;
}
若要使自己的容器也能支持 " { } " 方式初始化,需要在类中单独添加一个为 initializer_list 类准备的构造函数。
这里以这篇博客为例:(C++ STL)vector类的简单模拟实现与源码展示
在类中增加以 initializer_list 类参数的构造函数即可:
template<class Type> // 若类中已经有代表类型的关键字,
vector(std::initializer_list<Type> il) // 可去掉 template<class Type> 和
{ // 替换 std::initializer_list<Type> 中的 Type
for (const auto& e : il) // initializer_list 支持迭代器访问
{
push_back(e); // my::vector<T> 的成员函数
}
}
深拷贝与浅拷贝
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。
可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享。
如果 C++ 一个类中涉及到资源的管理,其 拷贝构造函数、赋值运算符重载 以及 析构函数 必须要显式给出。一般情况都是按照深拷贝方式提供。
写时拷贝
写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。
引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成 1,每增加一个对象使用该资源,就给计数增加 1,当某个对象被销毁时,先给该计数减 1,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。
具体细节可参考这位大佬的文章:
C++ STL string的Copy-On-Write技术
C++的std::string的“读时也拷贝”技术!