C++初始化探究

1.C++数据类型

参考C++标准,我认为数据类型可以分为这几大类型:

  • 基本数据类型
  • 类类型
  • 聚合体(非类类型的struct)
  • 数组

2.C++初始化写法

其实初始化符号和初始化类型就是对应的,问题在于某些不是使用对应符号时,却能触发的情况。

  • 无符号

    • 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}表示直接初始化,反正觉得这种语法尽量别用,太模糊了,还要自己分析;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值