C++变量和基本类型

C++变量和基本类型

1. 重定向问题

2. 初始化

2.1 初始化与赋值
  • 初始化不是赋值
  • 初始化是创建变量时赋予一个初始值。
  • 赋值是把对象的当前值擦除,用一个新值代替。
2.2 列表初始化

使用{}括起来的列表进行初始化,可以是直接初始化,也可以是拷贝初始化

当列表初始化应用于内置类型,且此时存在丢失信息的风险时,编译器会报错,注意与普通的初始化过程的对比

1
2
3
long double a = 10;
int b{a},c = {a}; //报错
int d(a),e = a; //正确

image-20220904230932730

2.3 默认初始化
  • 变量定义时未指定初值则会进行默认初始化。

  • 内置类型和复合类型(数组和指针)

    • 定义于块(函数作用域、类作用域)外初始化为0

    • 定义于块内不会初始化,为未定义,无法预知结果。

      1
      2
      3
      4
      5
      6
      7
      8
      #include<iostream>
      double a;
      int main(){
      double b;
      std::cout << a << std::endl; //函数外,初始化为0
      std::cout << b << std::endl; //函数内,编译器等运行环境决定
      return 0;
      }

      image-20220904224408106

  • 类类型调用类的默认构造函数进行初始化

2.4 值初始化

值初始化发生于以下三种情况

  • 数组初始化过程中提供的初始值少于数组的大小
  • 不使用初始值定义局部静态变量
  • 显式书写形如T()的表达式请求值初始化

值初始化的操作

  • 对于内置类型和复合类型,初始化为零
  • 对于数组,值初始化每个元素
  • 对于类类型,调用默认构造函数
2.5 直接初始化

不使用等号进行初始化,而是直接在类型右侧紧跟()或者{}来进行初始化

使用直接初始化的类型要有(){}内相应格式的构造函数

1
2
3
int a(1);
int b{1};
string c(10,'c');
2.6 拷贝初始化

使用等号进行初始化

1
2
3
int a = 1;
int b = {1};
string c = string(10,'c');

3. 声明与定义

  • 声明:规定变量的类型和名字

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    extern int i;		//初始化后成为定义,且不能在函数体内初始化
    //变量可以声明多次,但只能定义一次
    //例
    extern int i = 10; //定义,正确

    extern int i;
    int main(){
    i = 10; //错误
    return 0;
    }
  • 定义:规定变量的类型、名字,并申请存储空间,还可能赋初值。

    1
    int i;

4. 引用

  • 引用必须初始化,且无法进行赋值,即初始化之后无法重新绑定到其他对象上。

  • 引用不是对象,它只是对象的别名,引用的类型要和与之绑定的对象严格匹配。

  • 字面值和表达式的计算结果不是对象,因此没有相应的引用。也可理解为,引用是左值引用,即引用的对象必须是一个左值。

  • 引用必须绑定在对象上,不能定义引用的引用;引用不是对象,因此也没有引用的指针;指针是对象,因此有指针的引用。

    1
    2
    3
    4
    5
    6
    7
    8
    #include<iostream>
    int main(){
    int a = 10;
    int *b = &a;
    int *&c = b; //指针的引用,注意类型的匹配
    std::cout << *c << std::endl; //输出10
    return 0;
    }
  • 前述引用类型和与之绑定的对象类型要严格匹配。但是有两个例外

    • 初始化常量引用时,允许任意表达式作为初始值,只要该表达式结果能够转换为引用的类型即可。允许常量引用绑定非常亮的对象、字面值。

      1
      2
      3
      4
      5
      int a = 1;
      const int &b = a; //正确,非常量可以转换为常量
      const int &c = 1; //正确,字面值常量可以转换为常量对象
      const int &d = b * 2; //正确,运算结果可转换
      int &e = b * 2; //错误,不是常量引用,不能引用字面值

5. 指针

  • 指针是对象,指针的类型与它所指向的类型必须匹配。

  • 使用字面值nullptr初始化指针得到空指针,nullptr可以转换为任何指针类型。

  • void*指针可以指向任意类型的对象,不能直接操作void*指针指向的对象,因为无法确认对象的类型。

  • 使用*&等定义变量时,二者只是类型修饰符,是声明符的一部分,只作用于其后所接的第一个变量名。

    1
    2
    int i = 1024,*p = &i,&r = i;		//i为int型的数,p是int型指针,r是int型引用
    int *p1,p2; //p1为int型指针,p2为int型变量
  • 指针类型和所指对象类型不匹配的两种情况

    • 允许指向常量的指针指向一个非常量对象(类似常量引用)

      1
      2
      int a = 1;
      const int *b = &a; //正确,但是不能用b改变a的值

6. const限定符

  • const对象一经创建就无法修改,因此必须在定义时进行初始化

    • 对于有默认构造函数的类类型,可以使用默认初始化的方式

      1
      2
      3
      4
      5
      class Test{
      public:
      int a = 0;
      };
      const Test t1; //正确,使用默认初始化方式,调用默认构造函数
    • 对于其他类型,必须显式的进行初始化,即使用除了默认初始化之外的其他方式初始化

      1
      const int a;				//错误,不能使用默认初始化,必须显式初始化
  • const类型的对象上进行的操作不能改变其内容,否则会出错。

  • 默认情况下,const对象仅在一个文件中有效,因为const对象在编译时会被替换为其值,编译器必须知道它的初始值,因此每个文件中都必须有其定义。并且当多个文件中出现同名的const变量时,等同于在不同文件中定义了独立的变量。

  • 想要在文件之间共享const变量,可以使用extern关键字进行声明和定义

    1
    2
    3
    4
    //a.cpp中定义const变量
    extern const int bufSize = fcn();
    //b.h头文件中声明,即可直接使用
    extern const int bufSize; //与a.cpp中的是同一个变量
  • 常量引用,即把引用绑定到常量对象上。此时显然不能对被引用的常量对象进行修改操作。

    1
    2
    3
    const int a = 1;
    const int &b = a; //正确
    int &c = a; //错误
  • const的引用可以引用一个非const对象,此时无法通过引用修改对象,但是可以通过其他方式进行修改

    1
    2
    int i = 1;				//i是可以修改的
    const int &a = i; //不能通过a修改i对象
  • 指向常量的指针和常量指针

    • 指向常量的指针,不能用于修改其所指对象的值。对象不是常量时,其值可以通过其他方式修改。

      1
      2
      const int a = 1;
      const int *b = &a; //此处即为指向常量的指针,此时不能通过b修改a的值
    • 常量指针。指针是对象,可以把自身定为常量。常量指针要满足普通常量的要求(初始化等特性)。

      1
      2
      3
      4
      int a = 1;
      int *const b = &a; //此时b为常量指针,b的内容无法改变,但是a可以改变
      const int c = 1;
      const int *const d = &c; //指向常量对象的常量指针
  • 顶层const和底层const。顶层const表示指针本身是常量,底层const表示指针所指对象是常量。执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型可以相互转换。非常量可以转换为常量。

    1
    2
    3
    4
    5
    6
    7
    const int a = 1;
    const int *const b = &a; //既是底层const又是顶层const
    int c = 1;
    const int* d = &a; //底层const
    int *e = b; //错误,b包含底层const定义,而c没有
    d = b; //正确,b,c都包含底层const定义
    d = &c; //正确,int*可以转换为const int *

7. constexpr和常量表达式

  • 常量表达式是指值不会改变并且在编译过程中就可以得到计算结果的表达式。如:字面值、用常量表达式初始化的const对象。

    1
    2
    int a = 1;					//a不是常量表达式
    const int b = get_size(); //b不是常量表达式,编译时无法确定
  • constexpr用于声明常量表达式变量,声明之后,编译器就会在编译时验证该变量是否为常量表达式。声明为constexpr的变量一定是常量,并且必须用常量表达式初始化。

    1
    2
    constexpr int a = 1;		//1是常量表达式
    constexpr int b = size(); //此时size需要是一个constexpr函数
  • 声明constexpr时的类型必须是字面值类型。字面值类型包括算数类型、引用、指针、字面值常量类、枚举

  • constexpr指针

    • constexpr指针初始值必须是nullptr或者0,或者是存储于固定地址的对象。函数体内的变量存储于非固定地址,定义于所有函数体制外的对象存储于固定地址。

    • constexpr指针中constexpr限定符只对指针有效,与指针所指对象无关。使用constexpr定义变量相当于顶层const,定义指针相当于常量指针。

      1
      2
      3
      4
      const int *a = nullptr;		//指向常量的指针
      constexpr int *b = nullptr; //常量指针
      int p = 1;
      constexpr const int*c = &p; //指向常量的常量指针,p可以不是常量

8. 类型别名

  • 两种:typedefusing(别名声明)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //声明
    typedef int a; //a是int的同义词
    typedef a b,*c; //b是int的同义词,c是int*的同义词,*为类型修饰,表 示是指针类型
    //使用 出现原类型的地方直接使用别名代替即可

    //声明
    using a = int; //a是int的同义词
    //使用
    a b = 1;
  • 注意类型别名的理解

    1
    2
    3
    4
    typedef char *ptr;
    const ptr a = 0; //a是常量指针,指向char类型的对象
    const ptr *b; //ps是一个指针,指向对象是指向char的常量指针
    //ptr是整体,表示指向char的指针,此时不能直接将char*还原到语句中来理解。

9. auto类型说明符

  • auto由编译器分析表达式确定类型。因此auto定义的变量必须具有初始值,否则编译器无法分析。

    1
    auto a = b + c;			//根据b和c的类型确定a的类型
  • 当引用类型作为auto的初始值时,编译器会将引用对象的类型作为auto的类型。

  • auto会忽略顶层const,保留底层const。希望auto是顶层const,要明确指出。auto为引用类型时,引用的相应规则仍旧适用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    int i = 0;
    const int ci = i,&cr = ci;
    auto b = ci; //int
    auto c = cr; //cr是ci的别名,因此还是int
    auto d = &i; //int
    auto e = &ci; //ci为常量,&ci表示指向常量的指针的值(底层const),const int
    auto &g = ci; //const int,整形常量引用
    auto &h = 42; //错误
    const auto &j = 42; //正确,常量引用,const int

    const auto f = ci; //明确指出,ci是int,f即为const int
  • 使用auto一条语句声明多个变量时,因为一条声明语句只能有一个基本数据类型,因此该语句中所有变量的类型必须一致。

    1
    2
    3
    auto a = 0,b = 3.14;		//错误,a和b的类型不一致
    auto &m = ci,*p = &ci; //正确,m为常量引用,p为指向常量的指针
    auto &n = i,*p2 = &ci; //错误,n为int,p2为const int

10. decltype类型指示符

  • 用于希望从表达式类型推断要定义的变量类型但是不想用该表达式的值初始化的情形。

    1
    2
    decltype(f()) sum = x;			//f的返回类型即为sum的类型
    //编译器不会实际调用函数f,只会获取f的返回值的类型作为sum的类型
  • 如果decltype使用的表达式是一个变量,则会返回变量的类型,包括顶层const和引用。

    1
    2
    3
    const int a = 0,&b = a;
    decltype(a) x = 0; //const int
    decltype(b) y = x; //const int &
  • 如果decltype使用的表达式不是一个变量,则会返回表达式结果对应的类型。当表达式的结果对象能作为一条赋值语句的左值时,一般返回引用类型。

    1
    2
    3
    int i = 1;*p = &i,&r = i;
    decltype(r+0) b; //r是引用类型,直接替换为i即1,相加之后还是整形,因此b是int
    decltype(*p) c; //*p是一个可赋值的左值,返回引用类型,即int&,此时要初始化
  • 注意。对于decltype所用表达式,如果变量名加上额外的一对或多对括号,则编译器会将其当做表达式。并且是一种可以作为赋值语句左值的特殊表达式,因此会返回引用类型。

    1
    2
    3
    int i = 0;
    decltype((i)) a; //int &,必须初始化
    decltype(i) b; //int

11. 自定义数据类型

  • 类内数据成员可以进行显式初始化,如果没有,则会被默认初始化。
  • 类通常被定义在头文件中,而且类所在头文件的名字应该与类的名字一样。
  • 头文件通常包含只能定义一次的实体,如类、const、constexpr变量,还包含用到的其他头文件

12. 头文件保护预处理

  • #define把一个名字设定为预处理变量,设定后表示此变量已定义

  • #ifdef如果预处理变量已经定义,则执行此后的程序,直到#endif

  • #ifndef如果预处理变量没有定义,则执行此后的程序,直到#endif

  • 用预处理功能防止头文件重复包含.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //在头文件中加入如下预处理代码,可防止此头文件被多次引入

    #ifndef SALES_DATA_H //此时未定义,执行之后的程序
    #define SALES_DATA_H //定义,预处理变量名一般全部大写
    #include<string>
    struct Sales_data{
    std::string a;
    };
    #endif
    //之后如果再一次包含此头文件,则ifndef会为假,之后到endif的部分会被直接忽略

参考

  • 本文内容主要是C++ primer第二章内容的总结