1.C++数据类型
参考C++标准,我认为数据类型可以分为这几大类型:
- 基本数据类型
- 类类型
- 聚合体(非类类型的struct)
- 数组
2.C++初始化写法
其实初始化符号和初始化类型就是对应的,问题在于某些不是使用对应符号时,却能触发的情况。
-
无符号
- T t、T t = new T
默认初始化,全都适用;
- T t、T t = new T
-
类的初始化列表class c{ c(): m(_m){}};
-
()符号
- T t(val),T t(t1)显式初始化,基础数据类型和类类型可用,类类型常用;
- T t = T(),T t = new T()
值初始化,我更愿意称之为默认值初始化,
在提供的初始化器为空时执行,
适用于类类型(c++11,用default强制合成默认构造函数),
基础数据类型,聚合体,不适用于数组;
-
{}符号
- T t = {}(c++11起通用,原本只适用于聚合,数组应该不行)
- T t = {val}(c++11起通用,原本除聚合外,适用于基础数据类型)
- T t{} (c++11起)
- T t{val} (c++11起)
- T t = new T{} (c++11起)
在c++11之前,{}只适用于聚合体和数组,而且一定需要等号,
{val}也适用于基础数据类型,作收缩限制的语义;
在c++11起,成为了统一初始化符号,可以不带等号。而且{}和={}也适用于拥有默认构造函数(合成或自定义都行)的类类型,还有基础数据类型;然后{val}和=val适用于基础数据类型,还有定义了相应类型构造函数的类类型。还真的挺统一的;
-
=符号
作为辅助符号= {},= val,= T(), = t等等,
初始化时出现的=符号,和非初始化时出现的=符号,语义上是不同的;
3.C++初始化类型
- 默认初始化,不提供初始化器时执行。没啥好说的;
- 零初始化,初始化值为0,主要用于静态变量。C++ 98的时候,T()是0初始化,new T()是默认,坑的不行。【若 T 是非联合体类类型,则零初始化其所有基类和非静态数据成员,并初始化所有填充位为零位。忽略构造函数,若它存在】这条基本在static和全局变量类类型执行,调用默认构造函数 + 跳过构造函数进行值的0化;
- 值初始化,提供空的初始化器时执行。
我认为默认值初始化更贴切,我认为其实就是零初始化的封装,扩展语义后就可以兼容类类型,类类型的默认值初始化就是默认构造函数; - 显式初始化、复制初始化,这些其实都没啥问题,很明显和容易写;
- 聚合初始化,其实就是结构可以理解成异构数组,其实就是给一堆东西初始化,用 = {val0,val1…}来给每个元素提供初始化器;
- 统一初始化,c++11起。
关于如何看待c++标准里的初始化条款,不必真去琢磨太深,明白大概思想就行,那个真要理顺整套流程,达到自己可以敲出类似代码的话非常困难。所以,我觉得明白大概思想,然后疑惑时去查即可。
4.C++各类型如何接收初始化器
- 统一初始化,T t{}、T t = T{}、T t{val}、T t = T{val}、new T()、new T(val)、
new T{}、new T{val}; - 非统一初始化
- 基础数据类型,T t()、T t = T()、T t(val)、T t = T(val)、new T()、new T(val);
- 类类型,T t、new T、T t = T()、 new T()、T t = T(val)、new T(val);
- 聚合体,T t、new T、T t = T()、new T()、T t = {}、T t= {val};
- 数组,T t、new T、new T()、T t = {}、T t= {val};
其实感觉统一初始化出来之前,聚合体和数组的T t= {val},其实就相当于类类型的T t(val)和T t = T(val)。其实从符号上来说,就是类类型的默认值初始化方式,比聚合体的少了个={}。而显式初始化中,类类型用()符号,聚合体用={}符号。然后问题就来了,在new的时候,聚合体怎么赋初值呢?11之前new T{val}是编译不过的。我感觉统一初始化其中之一的原因,也是为了解决这个问题。
5.关于未显式初始化的局部变量的值
根据大佬翻找C99的标准:
- 未显式初始化的非静态局部变量的值是【不定的值】
- 【不定的值】是【未指定的值】或【陷阱标识】
- 【未指定的值】是类型有效值中的某一个,编译器决定
- 如果是【陷阱标识】这种实现,那么进行读操作是禁止的
虽然是C99的标准,但我用gcc和vs编译和跑了一下,基本情况也差不多。
Vs应该采用了【陷阱标识】这一实现,编译不过的;gcc采用了【未指定的值】这一实现,编译能过,结果输出为0,gcc中【未指定的值】应该是0。
找天去翻一下C++标准呢,虽然感觉基本没理解错。
6.值初始化的语法和效果
摘自:cppreference
语法:
T() (1)
new T () (2)
Class::Class(...) : member() { ... } (3)
T object {}; (4) (C++11 起)
T{} (5) (C++11 起)
new T {} (6) (C++11 起)
Class::Class(...) : member{} { ... } (7) (C++11 起)
效果:
值初始化的效果是:
1) 若 T 是有至少一个用户提供的任意种类的构造函数的类类型,则调用默认构造函数;
2) 若 T 是没有任何用户提供的构造函数的非联合体类类型,则值初始化 T 的每个非静态数据成员与基类组分;
(C++11 前)
1) 若 T 是没有默认构造函数,或拥有用户提供的或被删除的默认构造函数的类类型,则默认初始化对象;
2) 若 T 是拥有默认构造函数的类类型,而默认构造函数既非用户提供亦未被删除(即它可以是拥有隐式定义的或默认化的默认构造函数的类),则零初始化对象,然后若其拥有非平凡的默认构造函数,则默认初始化它;
(C++11 起)
3) 若 T 是数组类型,则值初始化数组的每个元素;
4) 否则,零初始化对象。
其实本质就是,区分是否让系统介入,我觉得c++11这样搞其实更乱。
某些特殊处理:
- 从 C++11 起,对没有用户提供的构造函数而拥有类类型成员的类进行值初始化,其中成员的类拥有用户提供的构造函数,会在调用成员的构造函数前对成员清零:
struct A
{
int i;
A() { } // 用户提供的默认构造函数,不初始化 i
};
struct B { A a; }; // 隐式定义的默认构造函数
std::cout << B().a.i << '\n'; // 值初始化 B 临时量
// C++03 中令 b.a.i 为未初始化
// C++11 中设 b.a.i 为零
// (注意 C++11 中 B{}.a.i 保留 b.a.i 为未初始化,但因为不同原因:
// 在 DR1301 后的 C++11 中,B{} 是聚合初始化,它值初始化拥有用户提供构造函数的 A)
- 当在成员函数中使用到,而在构造函数未进行初始化,且在成员函数直接使用时,会赋予一个0值。实际的值是0值,但不知道具体的含义是什么,因为0值也可以代表某些特殊值:
#include <iostream>
using namespace std;
class T {
public:
T() = default;
T(int _a){
a = _a;
}
int getA()
{
return a;
}
int a;
int b;
};
int main() {
// 进行了默认初始化,让编译器自动合成,所以成员变量本身是默认初始化的
T tb;
// 输出是0,如果把getA()删除,直接输出tb.a,那么会是无意义的随机值
cout << "tb.a: " << tb.getA() << endl;
return 0;
}
7. 暂定的自我规范
-
对于基础数据类型:
- 采用默认初始化(无符号);
- 使用复制初始化**(=符号**);
-
对于类类型:
- 采用默认初始化;
- 使用直接初始化((val)符号);
-
对于聚合类型和数组:
- 采用默认初始化;
- 采用复制式聚合初始化(={val1,val2,…}符号);
- 采用复制式聚合初始化,但非全值使后面的值被值初始化(={val1}(其实直接={}就会触发值初始化;
-
对于特殊的vector、map等STL:
- 在写demo或者特殊情况时,在c++11下使用复制式聚合初始化( = {val1,val2…}符号)
- 其他情况下同类类类型;
-
T t{}初始化的应用:
- 在c++11下,可配合=default,利用T t{}触发值初始化,让类内元素取默认值(基础数据类型会取0)。在c++03也可以出发,但是语法是Tt = T()过于古怪,还是算了。我觉得这个仅限于知道就好,尽量少用,不应该对值初始化/默认初始化这些隐式、没有可读性的东西,抱以任何期望;
- 我觉得因为这个语法糖,过多地变更初始化写法习惯,并无必要,少就是多;
-
尽量自己定义默认初始化函数,如非必要定义为空函数就可以,我的思想是不对隐式初始化抱有期望,默认初始化函数偏向懒惰;
-
关于{}的一些问题:
在T t{}表示值初始化,T t{val}表示直接初始化,反正觉得这种语法尽量别用,太模糊了,还要自己分析;