统一的列表初始化
{ } 初始化
C++11引入了统一的 列表初始化
(Uniform Initialization),这是一种使用大括号 { }
初始化变量和对象的新语法,旨在简化初始化过程并提高代码的可读性和一致性。
这种初始化方式适用于几乎所有类型,包括基本类型、用户自定义类型、聚合类型(如结构体和联合体)、以及STL容器等,从而解决了C++98/03中初始化方式不统一的问题。
它有以下几点好处:
- 方便,基本上可以替代普通括号初始化
- 可以使用初始化列表接受任意长度
- 可以防止类型窄化,避免精度丢失的隐式类型转换
下面深入看下列表初始化的几个用法:
示例:
1)基础数据类型
int a{10}; // 列表初始化
int a = { 10 }; // 列表初始化(也可以不使用等号)
2)初始化数组
int arr[] = {1, 2, 3};
int arr[]{1, 2, 3};
这里可以体现出 可以使用初始化列表接受任意长度 的特点。
3)类对象初始化,构造函数需要支持列表初始化
class Point
{
public:Point(int a, int b) : x{a}, y{b} {}
private:int x, y;
};Point p{1, 2}; // 使用花括号初始化对象
4)容器初始化
std::vector<int> vec = {1, 2, 3, 4};
std::vector<int> vec{1, 2, 3}; // 初始化向量
5)初始化智能指针
// 智能指针
std::unique_ptr<int> ptr{new int(5)}; // 初始化智能指针
6)防止类型窄化
int x{3.14}; // error,double转int会触发类型窄化
7)聚合类型的列表初始化
聚合类型是指没有用户定义的构造函数、没有私有或受保护的非静态数据成员、没有基类(包括虚基类)以及没有虚函数的类/结构体/联合体。对于聚合类型来说,列表初始化会直接按顺序初始化其成员。
struct Aggregate { int a; double b;
}; Aggregate agg{1, 2.3}; // 初始化a为1,b为2.3
类型窄化
类型窄化(Type Narrowing)是编程和计算机科学中的一个概念,它指的是将一个较大范围或精度的数据类型转换为一个较小范围或精度的数据类型的过程。这种转换通常会导致数据精度的损失或范围溢出,因为目标类型可能无法表示源类型的所有可能值。
在C++(以及许多其他编程语言)中,类型窄化通常发生在以下几种情况:
-
从浮点类型到整数类型的转换:
当你将一个浮点数(如float
或double
)转换为整数(如int
)时,小数部分会被丢弃,只保留整数部分。如果浮点数的绝对值大于目标整数类型的最大值,则会发生溢出,结果可能是未定义的(在C++中通常是实现定义的)。 -
从
long double
到double
/float
的转换,以及从double
到float
的转换:
这些转换涉及到浮点数精度的减少。例如,long double
类型可能支持比double
更多的有效数字,而double
又可能支持比float
更多的有效数字。因此,将long double
转换为double
或float
,或将double
转换为float
时,可能会丢失一些精度。然而,如果源值是常量表达式,并且这个值在目标类型的表示范围内,则这种转换是安全的,不会发生溢出或精度损失。 -
从整数或无作用域枚举类型到不能表示原类型所有值的整数类型的转换:
这种转换可能发生在将一个较大范围的整数类型(如long long
)转换为较小范围的整数类型(如int
)时。如果源整数的值超出了目标类型的表示范围,则会发生溢出,导致数据丢失或变成未定义的值(通常是实施定义的)。然而,如果源值是常量表达式,并且这个值完全能够存储在目标类型中,则这种转换是安全的。
类型窄化需要谨慎处理,因为它可能导致程序中出现难以调试的错误。为了避免这些问题,程序员应该:
- 尽可能避免不必要的类型窄化。
- 在进行类型窄化时,明确检查值是否在目标类型的表示范围内。
- 使用静态分析工具或编译器警告来帮助识别潜在的类型窄化问题。
- 考虑使用更宽的数据类型或特定的库函数来处理可能超出原始类型范围的值。
int main() {int a = 1.2; // okint b = {1.2}; // errorfloat c = 1e70; // okfloat d = {1e70}; // errorfloat e = (unsigned long long)-1; // okfloat f = {(unsigned long long)-1}; // errorfloat g = (unsigned long long)1; // okfloat h = {(unsigned long long)1}; // okconst int i = 1000;const int j = 2;char k = i; // okchar l = {i}; // errorchar m = j; // okchar m = {j}; // ok,因为是const类型,这里如果去掉const属性,也会报错
}
分析:
1. 整数类型初始化
int a = 1.2; // ok
int b = {1.2}; // error
- 第一行是合法的,因为C++允许从浮点类型隐式转换为整数类型,这里
1.2
会被截断为1
。 - 第二行是错误的,因为使用大括号初始化(列表初始化)时,不能从浮点数隐式转换为整数,这要求类型完全匹配或至少是兼容的转换。
2. 浮点数溢出
float c = 1e70; // ok,但可能不准确
float d = {1e70}; // error
- 第一行虽然编译通过,但
1e70
远远超出了float
类型的表示范围,因此结果可能不是精确的1e70
,而是最接近float
能表示的值。 - 第二行同样因为列表初始化要求类型精确匹配或兼容的转换,而
1e70
作为double
字面量不能直接列表初始化给float
。
3. 无符号整数到浮点数的转换
float e = (unsigned long long)-1; // ok
float f = {(unsigned long long)-1}; // error
float g = (unsigned long long)1; // ok
float h = {(unsigned long long)1}; // ok
- 第一行和第二行展示了类型转换与列表初始化的区别。第一行中,
(unsigned long long)-1
被转换为float
,这是一个有效的转换(尽管可能会丢失精度)。 - 第二行错误,因为列表初始化不允许这种隐式转换。
- 第三行和第四行都展示了从
unsigned long long
到float
的有效转换,第四行之所以可以是因为(unsigned long long)1
的值在float
的表示范围内。
4. 常量整数到字符的转换
const int i = 1000;
const int j = 2;
char k = i; // ok,但可能导致溢出或截断
char l = {i}; // error
char m = j; // ok
char m = {j}; // 重复定义,忽略此错误讨论
char k = i;
是合法的,但可能会导致值被截断为char
类型能表示的最大值(如果i
的值超出char
的表示范围)。char l = {i};
错误,因为列表初始化不允许从超出char
表示范围的const int
隐式转换。char m = j;
是合法的,因为j
的值在char
的表示范围内。
语法区分1
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;
};Date d1 {2023, 5, 1};
Date d2 = {2023, 5, 1};Date d1(2023, 5, 1);
// 语法错误
Date d2 = (2023, 5, 1);
Date d1(2023, 5, 1)
这种写法的本质是调用 构造函数 ;Date d2 = {2023, 5, 1}
这种写法的本质,相当于先调用 构造函数 并使用 {2023, 5, 1} 构造一个对象,再调用 拷贝构造 来完成d2的构造,最终会被编译器优化为直接使用 {2023, 5, 1} 构造 d2。本质是一个多参数的隐式类型转换,需要调用一个多参数的构造函数,而调用一个多参数的构造函数必须使用 { } ;
常见的隐式类型构造,有单参数的隐式类型构造,如:
std::string s1 = "hello"; // 单参数的隐式类型构造std::string s2("hello"); // 调用构造函数
语法区分2
// 构造
Date* darr1 = new Date[3]{ d1, d2, d3 };
// 构造 + 拷贝构造 -- 优化
Date* darr2 = new Date[3]{ {2024,3,23}, {2824,3,23}, {2024,3,23} };
// 构造
Date* darr3 = new Date(2023,3,34);
// 构造 + 拷贝构造 -- 优化
Date* darr4 = new Date{ 2023,3,34 };
总结: 圆括号只能在调用构造函数被使用。
std::initializer_list
看一下,下面 d1
和 v1
定义时,是否使用了同样的语法?
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 = { 2023, 5, 1 };vector<int> v1 = { 1, 2, 3, 4, 5, 6 };return 0;
}
答:使用的语法是不同的。
- 对于d1来说,上述写法调用的是Date的构造函数,并且只能使用三个参数进行构造。
- 对于v1来说,在进行构造时,是可以指定 不定个数 的初始值。可见,两者使用的不是同一个语法。实际上,v1使用的是
initializer_list
语法。
C++11引入了 std::initializer_list
,主要用于处理编译时期未知大小的初始化列表。
std::initializer_list
是一个轻量级的类模板,定义于头文件<initializer_list>
中,它可以表示一个特定类型的常量值的列表,这些值在编译时确定并在运行时保持不变。
- v1 实际上将 { 1, 2, 3, 4, 5, 6 } 部分作为参数传递给
initializer_list
进行构造一个initializer_list<int>
类型的对象,然后在使用这个对象构造vector。 - d1实际上是先调用
Date
的构造函数再调用拷贝构造,最终被编译器优化为直接调用构造函数。
同时,如果想让vector使用以下用法:
std::vector<int> v1 = { 1, 2, 3, 4 };
std::vector
底层必须实现类似于如下的构造函数:
vector(initializer_list<T> il)
{reserve(il.size()); // 开辟空间for(auto& e : il){push_back(e);}
}
类似用法
std::pair<std::string, std::string> kv1("sort", "排序");
std::pair<std::string, std::string> kv2("string", "字符串");std::map<std::string, std::string> dict1 = { kv1, kv2 };
std::map<std::string, std::string> dict2 = { {"sort", "排序"}, {"string", "字符串"} };
生成 dict2 的基本步骤为:
-
先调用pair的构造函数,使用 {“sort”, “排序”} 构造一个
pair<const char*, char*>
类型的匿名对象 -
再使用pair对象作为
initializer_list
的参数构造一个对象,这个initializer_list的类型为initializer_list<pair<const char*, char*>>
-
map的类型为
<string, string>
,该类型对应的initializer_list
类型为initializer_list<pair<const string, string>>
-
两者类型不相同,但是由于pair的拷贝构造函数使用了模板参数,所以可以进行转换
详细说明:
从左边向右推:
-
首先,使用
initializer_list
创建一个map<string, string>
对象,就需要一个initializer_list<pair<const string, string>>
参数; -
再来看右边的用于初始化的值:{“sort”, “排序”},会先调用
pair
的构造函数生成pair<const char*, char*>
类型的匿名对象; -
{ {“sort”, “排序”}, {“string”, “字符串”} } 会生成一个
initializer_list<pair<const char*, char*>>
对象 -
最后,将右边
initializer_list<pair<const char*, char*>>
对象作为参数传递给 map的构造函数initializer_list<pair<const string, string>>
进行拷贝构造时,会发现类型不匹配,普通的函数到这一步会产生报错行为,但是因为pair的拷贝构造函数使用了函数模板,这就表明在拷贝构造一个pair对象时,传入对象的两个参数不用和 first 和 second 完全一样,只要该对象的两个参数可以分别构造 first 和 second 即可。
底层类似于:
template<class T1, class T2>
struct pair
{// 拷贝构造函数template<class U, class V)pair(const pair<U, V>& kv):first(kv.first), second(kv.second){}private:T1 first;T2 second;
};
如果进一步使用 pair<const char*, char*>
拷贝构造一个类型为 pair<const string, string>
对象时,也就是 template<class U, class V)
的模板参数为 template<const char*, char*)
:
pair(const pair<const char*, char*> kv):first(kv.first), second(kv.second)
{}
这里的 first 和 second 都是 string
类型的变量,这里可以使用 char* 对象初始化string,因为对于 string 类型的对象可以使用 char* 类型的对象进行构造:
所以最终就可以构造成功。
如果pair的拷贝构造函数没有使用函数模板,即:
template<class T1, class T2>
struct pair
{// 拷贝构造函数pair(const pair<T1, T2>& kv):first(kv.first), second(kv.second){}
private:T1 first;T2 second;
};
这样 T1的类型为const string
, T2的类型为string
,当将pair<const char*, char*>
类型对象作为参数传递给 kv 时,就会显示没有合适的构造函数,而出错。
小结:这里列举了
pair
的用法:可以使用pair<const char*, char*>
类型的对象初始化pair<const string, string>
(其底层原因在于pair的拷贝构造使用了函数模板)。
pair
的构造函数和拷贝构造函数类似于:
template<class T1, class T2>
struct pair
{// pair<const char*, char*> kv1("sort", "排序");pair(const T1& t1, T2& t2):_first(t1), _second(t2){}// pair<const string, string> kv2(kv1);template<class U, class V>pair(const pair<U, V>& kv):_first(kv._first), _second(kv._second){}
private:T1 _first;T2 _second;
};
原理
std::initializer_list
在底层的实现通常涉及到一对指针。这是因为 std::initializer_list
的设计目标之一是在不复制数据的情况下提供对初始值列表的访问,以提高效率。
具体来说,std::initializer_list
包含两个指针,一个指向列表中的第一个元素,另一个指向列表结束后的下一个位置(通常称为“past-the-end”指针),这与 std::vector
或其他一些容器的迭代器行为相似。
在 C++11 到 C++14 的标准中,std::initializer_list
实现为指向一个临时数组的指针。这个数组是由编译器生成的,用于存储初始化列表中的元素。由于这些元素是从初始化列表直接构造的,因此 std::initializer_list
的构造并不涉及数据的复制,而是直接使用了这些元素的地址。
在 C++14 中,底层的数组可以被放置在不同的存储区域,包括自动、静态或只读内存,这取决于具体的上下文和编译器优化策略。
声明
auto
关键字允许编译器根据初始化表达式自动推导变量的类型,简化了代码并提高了可读性。(尽量不要使用auto做返回值)decltype
用于获取一个表达式的类型,常用于模板编程和返回类型推导。
auto
在C++98中 auto
是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部与中定义的局部变量默认就是自动存储类型,所以auto
就没什么价值了。
C++11中废弃了auto
原来的用法,将其用于实现变量的自动类型推断。这样要求必须进行显式初始化,让编译器将定义对象的类型设置为初始化值的类型。
#include <map>
#include <string>int main()
{map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };// map<string, string>::iterator it = dict.begin();auto iter = dict.begin();return 0;
}
decltype
typeid
我们可以使用 typeid
得知一个变量和类型的底层类型:
#include <map>
#include <string>int main()
{map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };//map<string, string>::iterator it = dict.begin();auto iter = dict.begin();std::cout << typeid(iter).name() << std::endl;return 0;
}
但我们发现结果并不是我们预期那样,结果显示的是最底层的类型:
class std::_Tree_iterator<class std::_Tree_val<struct std::_Tree_simple_types<struct std::pair<class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > const ,class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > > > > >
而且 typeid.name()
是以字符串的形式返回一个变量的类型,也就是说虽然我可以得知一个变量的底层类型,但是我不能直接根据typeid.name()
的返回值来声明一个变量。这个工作可以使用 decltype
完成。
#include <map>
#include <string>int main()
{map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };//map<string, string>::iterator it = dict.begin();auto iter = dict.begin();// 不能使用:typeid(iter).name() iter2 = dict.end();decltype(iter) iter2 = dict.end();return 0;
}
下面将详细介绍一下 decltype
。
decltype
用于获取一个表达式的类型,常用于模板编程和返回类型推导。
decltype的特点
- 编译时类型推导:
decltype
在编译时确定表达式的类型,而不需要在运行时进行实际计算。这使得它能够在不知道表达式具体类型的情况下编写更通用的代码,特别是在模板编程中非常有用。 - 不计算表达式:
decltype
仅仅推导表达式的类型,而不会实际计算表达式的值。这意味着它可以用于可能产生副作用的表达式,而不会引发这些副作用。 - 保留引用和const属性: 如果
decltype
中的表达式是一个变量,并且该变量具有引用或const属性,则这些属性会被保留在推导出的类型中。 - 支持复杂表达式:
decltype
可以用于任何合法的C++表达式,包括变量、函数调用、算术运算、成员访问等。这使得它能够在复杂类型推导场景中发挥作用。
关于 “不计算表达式” ,这一点的理解:
允许编译器基于表达式的类型来推导出一个类型,而无需执行该表达式本身。
-
不产生副作用:使用
decltype
时,传递给它的表达式不会被执行(即不会计算表达式的值),因此该表达式不会产生任何副作用。例如,如果表达式包含函数调用、自增(++
)、自减(--
)等操作,这些操作的实际效果(即调用函数、改变变量值等)都不会发生。 -
仅用于类型推导:
decltype
的主要目的是根据表达式的类型来推导出一个类型,而不是去计算或执行该表达式的值。这意味着你可以安全地使用decltype
来查询任何表达式的类型,而不用担心该表达式可能带来的副作用或性能开销。 -
保持类型信息:
decltype
能够准确地保持表达式的类型信息,包括复杂的表达式类型、引用类型等。例如,如果表达式是一个对变量的引用,那么decltype
推导出的类型也会是一个引用类型。(如+=
的返回值为&引用类型
)
int main()
{int a = 10;decltype(a) b = 20; // b 被推断为 intint a = 1;decltype(a++) c = 30; // c 被推断为 intstd::cout << a << std::endl; // 结果为1return 0;
}
关于 “保留引用和const属性” ,这一点的理解:
#include <map>
#include <string>int main()
{const int b = 2;int a = 1;const int* p = &a;std::cout << typeid(p).name() << std::endl; // int const*std::cout << typeid(b).name() << std::endl; // intreturn 0;
}
当 const
修饰的是变量本身将不保留,当 const
修饰不是变量本身则保留。
typeid的特点
- 运行时类型信息: 与
decltype
不同,typeid
在运行时获取表达式的类型信息。它返回一个std::type_info
对象的引用,该对象包含了表达式的实际类型信息。 - 支持类型比较: 通过
std::type_info
对象的operator==
和operator!=
成员函数,可以比较两个类型是否相同。这在进行类型检查和类型转换时非常有用。 - 忽略cv限定符:
typeid
会忽略类型的const
和volatile
限定符,即typeid(const T) == typeid(T
)总是返回true
。 - 操作对象广泛:
typeid
的操作对象既可以是表达式,也可以是数据类型。它可以用于获取变量、对象、内置类型、自定义类型(如结构体和类)以及表达式的类型信息。 - 返回类型信息的表示:
typeid
返回的std::type_info
对象提供了name()
成员函数来返回类型的名称(但需要注意的是,这个名称可能会因编译器而异,且可能不是人类可读的)。此外,在一些编译器中,还提供了hash_code()
成员函数来返回类型的唯一哈希值。
decltype
的应用场景:当不明确某个变量的具体类型,但是又需要使用这个类型进行模板实例化:
vector<decltype(ret)> a;
结合 auto
和 decltype
,在需要复杂类型推断时,可以使用:
auto x = 10;
decltype(x) y = 20; // y的类型与x一样,为int
nullptr
由于C++中NULL
被定义成字面量0,这样就可能回带来一些问题:因为0既能表示指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr
,用于表示空指针。
C++为什么要使用nullptr而不是NULL
主要原因是 nullptr
有明确的类型,它是 std::nullptr_t
类型,可以避免代码中出现类型不一致的问题。
1)类型安全: NULL
通常被定义为数据 0
(在C++代码中一般是 #define NULL 0
),它实际上是整型值。这就可能会带来类型不一致的问题,比如传递参数时,编译器无法准确判断是整数 0
还是空指针。而 nullptr
则是 std::nullptr_t
类型的,能够明确表示空指针,是编译器更容易理解代码。
2)代码可读性: 使用 nullptr
使得代码更具有可读性和可维护性。他明确传达了变量用作指针而非整型值,例如:
void process(int x)
{std::cout << "Integer: " << x << std::endl;
}void process(void* ptr)
{std::cout << "Pointer: " << ptr << std::endl;
}int main()
{process(NULL); // int 还是指针?process(nullptr); // 指针return 0;
}
在上面的代码中,可以看出 nullptr
能让编译器和程序员清楚地知道调用哪个函数。
3)避免潜在的问题: 在函数重载和模板中使用 NULL
可能导致编译器选择错误的重载版本。另外模板编程中特别是涉及类型推断时,NULL
会带来一些不期望的效果。
template<typename T>
void foo(T x)
{std::cout << typeid(x).name() << std::endl;
}int main() {foo(0); // 0 是int型foo(NULL); // 你希望是int还是指针呢foo(nullptr); // std::nullptr_treturn 0;
}
在上面代码中,使用 nullptr
可以让我们精确控制模板的类型。
using 和 typedef
介绍
using
在C++11中引入, using
和 typedef
都可以用来为已有的类型定义一个新的名称。最主要的区别在于,using
可以用来定义模板别名,而 typedef
不能。
1)typedef
主要用于给类型定义别名,但是它不能用于模板别名。
typedef unsigned long ulong;
typedef int (*FuncPtr)(double); // 函数指针
2)using
可以取代 typedef
的功能,语法相对简洁。
using ulong = unsigned long;
using FuncPtr = int (*)(double);using func_t = std::function<void(const std::string& name)>;
3)对于模板别名,using
显得 非常强大且直观。
template<typename T>
using Vec = std::vector<T>;
总之,更推荐使用 using
,尤其是当你在处理模板时。
扩展知识
1)模板别名(Template Aliases): using
在处理模板时,如定义容器模板别名,非常方便。假如我们需要一个模板类 std::vector
的别名:
template<typename T>
using Vec = std::vector<T>;
Vec<int> vecInt; // 相当于 std::vector<int> vecInt;
2)作用范围: using
还可以用于命名空间引入,typedef
没有此功能。
using namespace std;
注意: 如果想使用 using
给命名空间取别名可能有些问题,所以这种场景下最好使用 namespace
:
namespace LongNamespaceName
{int value = 0;void setValue(int newVal) {value = newVal;}int getValue() {return value;}
}
// 使用namespace个命名空间取别名
namespace LNN = LongNamespaceName;int main() { LNN::setValue(42); // 使用函数来设置值 int myValue = LNN::getValue(); // 使用函数来获取值 return 0;
}
3)可读性与调试: using
相对 tyypedef
更易读。
typedef void (*Func)(int, double);
using Func = void(*)(int, double);
关于
define、typedef和using的用法对比,可以参考一下这篇文章:define、typedef和using的用法
范围for循环
关于这部分介绍,主要参考这篇博客:参考链接
范围for的语法
若是在C++98中我们要遍历一个数组,可以按照以下方式:
int main()
{int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };//将数组元素值全部乘以2for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++){arr[i] *= 2;}//打印数组中的所有元素for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++){cout << arr[i] << " ";}cout << endl;return 0;
}
以上方式也是C语言中所用的遍历数组的方式,但对于一个有范围的集合而言,循环是多余的,有时还容易犯错。
C++11中引入了基于范围的for循环,for循环后的括号由冒号分为两部分,第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。比如:
int main()
{int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };//将数组元素值全部乘以2for (auto& e : arr){e *= 2;}//打印数组中的所有元素for (auto e : arr){cout << e << " ";}cout << endl;return 0;
}
注意: 与普通循环类似,可用continue来结束本次循环,也可以用break来跳出整个循环。
范围for的使用条件
一、for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
二、迭代的对象要支持 ++
和 ==
操作
范围for本质上是由迭代器支持的,在代码编译的时候,编译器会自动将范围for替换为迭代器的形式。而由于在使用迭代器遍历时需要对对象进行 ++
和 ==
操作,因此使用范围for的对象也需要支持 ++
和 ==
操作。
STL中的一些变化
新容器
C++11中新增了四个容器,分别是array
、forward_list
、unordered_map
、unordered_set
。
一、array容器
array 容器本质就是一个静态数组,即固定大小的数组。
array容器有两个模板参数,第一个模板参数代表的是存储类型 ; 第二个模板参数是一个非类型模板参数,代表的是数组中可存储元素的个数。如:
#include <iostream>
#include <array>
int main()
{std::array<int, 10> a1; // 定义一个可存储10个int类型元素的array容器std::array<double, 5> a2; // 定义一个可存储5个double类型元素的array容器return 0;
}
array容器与普通数组
- array容器与普通数组 一样,支持通过
[ ]
访问下标的元素,也支持使用范围for遍历数组元素,并且创建后数组的大小也不可改变。 - array容器与普通数组不同之处就是,array容器用一个类对数组进行了封装,并且在访问array容器中的元素会进行越界检查。用
[ ]
访问元素时采用断言检查,调用at
成员函数访问元素时采用抛异常检查。 - 对于普通数组来说,一般只有对数组进行写操作时才会检查越界,如果只是越界进行读操作可能并不会报错。
- array容器的对象是创建在栈上的,因此array容器不适合定义太大的数组。
array特性
- 固定大小:
std::array
是一个固定大小的序列容器,一旦创建了,其大小就不能改变,它使用的是栈内存。它与std::vector
不同,std::vector
是动态大小的。 - 性能优势:
std::array
在性能上很接近于C语言风格的数组,因为它使用连续的栈内存布局。 - 类型安全: 与C语言风格的数组相比,
std::array
提供了类型安全的at()
接口。 - 接口友好:
std::array
提供了STL容器的标准接口,如size()
、begin()
、end()
等,使用上也非常方便。 - 与现代C++特性结合: 作为STL的一部分,
std::array
可以很自然地和其他标准库功能配合使用,比如范围for 循环、算法函数等。
对比
- **与C风格数组对比:**虽然C风格数组在声明时看起来更简单,但是它们不支持拷贝赋值和交换操作,容易出现越界问题,不提供大小信息。而
std::array
则具有这些优势。 - 与其他STL容器对比:
std::array
和std::vector
都是数组类型的容器,但std::vector
是动态大小的,可以在运行时调整长度,在需要动态容纳元素的场合非常有用。但如果你确定数组长度不会改变,选择std::array
会更高效。
#include <iostream>
#include <algorithm>
#include <array>
int main()
{std::array<int, 5> a{ 1, 2, 3, 4, 5 };// 使用范围for进行遍历for (int num : a)std::cout << num << " ";// 使用算法库中的排序算法(升序)std::sort(a.begin(), a.end(), std::greater<int>());std::cout << std::endl;for (int num : a)std::cout << num << " ";return 0;
}
二、forward_list容器
forward_list
容器本质就是一个单链表。
forward_list
很少使用,原因如下:
forward_list
只支持头插头删,不支持尾插尾删,因为单链表在进行尾插尾删时需要先找到尾部节点,时间复杂度为O(N)。forward_list
提供的插入函数叫做insert_after()
,也就是在指定元素的后面插入一个元素,而不像其他容器是在指定元素的前面插入一个元素,因为单链表如果要在指定元素前面插入元素,就需要找到前一个节点元素,这样就需要遍历链表,时间复杂度为O(N)。forward_list
提供的删除函数叫做erase_after()
,也就是删除指定元素后面的一个元素,而不像其他容器是删除指定元素,因为单链表如果要删除指定元素,就需要找到前一个节点元素,这样也需要遍历链表,时间复杂度为O(N)。
因此,一般情况下要用链表我们还是选择使用 std::list
容器。
三、unordered_map和unordered_set容器
unordered_map
和unordered_set
容器底层采用的都是哈希表。
这两个容器在博客主页中会详细介绍~
字符串转换函数
C++11提供了各种内置类型与string之间相互转换的函数,比如 to_string
、stoi
、stol
、stod
等函数。
一、内置类型转换为string
将内置类型转换成std::string
类型统一调用to_string()
函数,因为to_string函数为各种内置类型重载了对应的处理函数。
二、string转换成内置类型
如果要将string
类型转换成内置类型,则调用对应的转换函数即可。详细信息链接
容器中的一些新方法
C++11为每个容器都增加了一些新方法,比如:
- 提供了一个以
initializer_list
作为参数的构造函数,用于支持列表初始化。 - 提供了
cbegin
和cend
方法,用于返回const迭代器。 - 提供了
emplace
系列方法,并在容器原有插入方法的基础上重载了一个右值引用版本的插入函数,用于提高向容器中插入元素的效率。
注: emplace
系列方法和新增的插入函数提高容器插入效率的原理,涉及C++11中的右值引用、移动语义和模板的可变参数等机制,后续讲解~。
面试说明
在面试中,当被要求讨论C++的设计缺陷时,重要的是要展现你对C++语言特性的深入理解,同时也要展示出你能够批判性地思考这些特性及其潜在问题。你提到的几个点中,有些确实是C++中需要注意的复杂性或潜在问题,但并非所有都直接归类为“设计缺陷”。下面我将针对你提到的几点进行逐一分析和阐述:
-
多继承与菱形继承:
- 设计缺陷:多继承虽然提供了灵活性,但也带来了复杂性,尤其是菱形继承(Diamond Inheritance)问题。菱形继承会导致多个基类中的相同成员在派生类中有多个副本,这不仅浪费内存,还可能引起二义性问题。
- 解决方案:C++11引入了虚继承(virtual inheritance)来解决菱形继承中的二义性和数据冗余问题。然而,虚继承也增加了额外的间接层次,可能影响性能。
-
类和对象设计的复杂性:
- 设计缺陷:C++的类和对象模型非常强大但也相当复杂,特别是涉及到构造函数、析构函数、拷贝构造函数、赋值操作符等。如果开发者没有正确理解这些特殊成员函数的行为,很容易导致资源泄露、重复释放等错误。
- 解决方案:使用智能指针(如
std::unique_ptr
、std::shared_ptr
)管理资源,遵循“三/五法则”(Rule of Three/Five)来确保资源正确管理。
-
auto作为返回值:
- 不是设计缺陷:
auto
作为返回类型实际上是C++11的一个增强特性,它允许编译器自动推导函数的返回类型,这减少了类型声明的冗余,并提高了代码的可读性和可维护性。
- 不是设计缺陷:
-
一切皆可用列表初始化:
- 不是设计缺陷:统一初始化(Uniform Initialization)是C++11引入的,它允许使用花括号
{}
来初始化各种类型的对象,这有助于避免一些类型推导上的错误,同时也使得代码更加统一和易于阅读。
- 不是设计缺陷:统一初始化(Uniform Initialization)是C++11引入的,它允许使用花括号
-
std::forward_list
:- 不是设计缺陷:
std::forward_list
是C++11标准库中的一个容器,它表示一个单向链表。它提供了高效的插入和删除操作,但随机访问性能较差。这更多的是关于容器的选择问题,而不是C++语言的设计缺陷。
- 不是设计缺陷:
总结时,你可以这样说:“C++作为一个功能强大的编程语言,提供了丰富的特性和灵活性,但同时也带来了一些复杂性。比如,多继承和菱形继承虽然提供了设计上的灵活性,但也容易引发二义性和资源冗余问题。C++的类和对象模型非常强大,但也需要开发者仔细处理特殊成员函数以避免资源泄露等问题。相比之下,C++11引入的auto
作为返回类型、统一初始化等特性则增强了语言的表达能力和易用性,并非设计缺陷。而std::forward_list
作为标准库中的一个容器,其设计旨在满足特定场景下的需求,也不应被视为语言的设计缺陷。”
今天的分享就到这里了,如果,你感觉这篇博客对你有帮助的话,就点个赞吧!感谢感谢……
原文链接:https://blog.csdn.net/chenlong_cxy/article/details/126690586