跳转至

Basic Syntax

约 2411 个字 14 行代码 预计阅读时间 8 分钟

Initialization

初始化是指在创建对象(为特定类型的对象申请存储空间)的同时赋初始值。C++ 的初始化方式与规则五花八门,大概包括以下几种:直接初始化、拷贝初始化、列表初始化、默认初始化、值初始化、类内初始值、构造函数初始值列表。

一般有着四类初始化方式,现代 C++ 的内置类型和类类型都支持这四种初始化方式:

  • 等号:int a = 1; 或者 std::string s = "hello";
  • 等号加花括号:int a = {1}; 或者 std::string s = {"hello"};
  • 花括号:int a{1}; 或者 std::string s{"hello"};
  • 圆括号:int a(1); 或者 std::string s("hello");
  1. 默认初始化/Default Initialization:当对象未被显示地赋予初值时执行的初始化行为。

    • 类类型:由类的默认无参构造函数决定,如果没有默认无参构造函数,则该类不支持默认初始化。
    • 内置类型(指针、intdoublefloatboolchar 等)及其数组:
      • 全局(包括定义在任何函数之外、命名空间之内的)变量或局部静态变量:初始化为 0(这种情况也叫值初始化);
      • 局部非静态变量或类成员:未定义(未初始化)。
    1
    2
    3
    4
    int i;                    // 默认初始化:未定义
    std::string s;            // 默认初始化:调用默认构造函数,得到空字符串
    MyClass* p = new MyClass; // 默认初始化:全看构造函数
    double* pd = new double   // 默认初始化:0.0
    
  2. 值初始化/Value Initialization:默认初始化的特殊情况,此时内置类型会被初始化为 0。基本场景:

    • STL 容器只指定元素数量,而不指定初值时,就会执行值初始化,如 vector<int> vec(10);:10 个 int,初始化为 0
    • 全局(包括定义在任何函数之外、命名空间之内的)变量或局部静态变量:初始化为 0;
    • new 类型,后面带括号,如:new int(), new string{};
    • 初始值列表为空 {},如 double d{};int *p{};

    对于类类型,其实不需要区分默认初始化和值初始化,因为类类型的初始化只决定于构造函数,与对象在函数内/外、全局/局部/类成员、静态/非静态、默认初始化/值初始化无关。

Iterator

Functions

1 Basics

一个典型的函数包括:返回类型、函数名字、形参列表与函数体。函数也需要像其余的名字一样在使用前声明,函数声明也称为函数原型/Function Prototype。使用调用运算符来执行函数,调用运算符的形式是一对圆括号 (),运算符作用于一个表达式,这个表达式是函数或者指向函数的指针,调用表达式的类型就是函数的返回类型。函数的返回类型不能为数组类型或者函数类型,但可以是指向数组或者函数的指针。

函数的调用其实完成了两项工作:一个是使用实参初始化函数对应的形参,另一个是将控制权从主调函数转移到被调函数。而返回语句也完成两项工作:一个是返回 return 语句中的值,另一个是将控制权从被调函数转移到主调函数。

当形参列表为空的时候,可以直接书写一个空的形参列表,这样就隐式定义了空形参列表,但是与 C 语言兼容的形式是显式使用 void 关键字。

在 C++ 中,名字有作用域,对象有生命周期/Lifetime,作用域是程序文本的一部分,名字在作用域中可见,对象的生命周期是程序执行过程中对象存在的一段时间。

函数体是一个语句块,块构成一个新的作用域。函数的形式参数和函数体内部定义的变量统称为局部变量,局部变量只有在函数的作用域内可见,并且会隐藏在外层作用域中同名的其他声明。所有在函数体外定义的对象存在于程序的整个执行过程之中,在程序启动时被创建,直到程序结束才会被销毁,局部变量的的生命周期依赖于定义的方式:

  • 自动对象/Automatic Object:只存在于块执行期间的对象。当函数的控制路径经过变量定义语句的时候创建该对象,当到达定义所在的块末尾的时候销毁它,当块执行结束后,块中创建的自动对象的值就变成未定义的了。形参就是一种自动对象,在函数开始的时候对形参申请存储空间,函数终止就被销毁。有初始值的时候,自动对象的值就是初始值,没有初始值的时候,自动对象就执行默认初始化。
  • 局部静态对象/Local Static Object:在程序的执行路径第一次经过对象定义语句的时候初始化,并且知道程序终止的时候才会被销毁,在此期间即使对象所在的函数/块结束执行也不会对其有影响。将局部变量定义为 static 类型就可以得到这样的对象。当局部静态对象没有初始值的时候执行值初始化。

C++ 与现代编译器支持分离式编译/Separate Compilation。

2 Argument Passing

5 Features for Specialized Uses

5.1 Default Argument

很多函数有这样一种形参,在函数的很多次调用中都被赋予一个相同的值,我们将这个反复出现的值成为函数的默认实参/Default Argument,调用含有默认实参的函数时,可以包含这个实参,也可以省略该实参。默认实参只能出现在形参列表的尾部,一个形参一旦被赋予了默认实参,它后面的所有形参都必须有默认实参。在给定的作用域中,一个形参只能被赋予一次默认实参,函数的后续声明都只能为之前那些没有默认值的形参提供默认实参。

对于已经有默认实参的函数 std::string screen(size_t ht = 24, size_t wid = 80, char backgrnd = ' '),函数调用时实参按照其位置解析,默认实参负责填补函数调用缺少的尾部实参。若想要覆盖中间的某个默认实参(如 wid),就需要为前面的所有形参提供实参。

局部变量不可以作为默认实参,除此之外,只要表达式的类型可以转换成形参所需要的类型,就可以作为默认实参。默认实参的名字在函数声明所在的作用域内解析,名字的求值是在函数调用时进行的,而不是在函数声明时。

5.2 Inline Function and constexpr Function

定义在类内的成员函数会被编译器隐式地视为内联函数,但是是否真正内联由编译器决定。

5.3 Debugging

7 Pointers to Functions

首先需要说明:函数指针指向的是函数而非对象,函数指针指向某种特定类型,函数的类型由它的返回类型和形参类型共同决定,与函数名无关。函数指针的声明形式为:return-type (*pointer-name)(parameter-list),其中 return-type 是函数的返回类型,parameter-list 是函数的形参列表,pointer-name 是函数指针的名字。

若我们定义如下的函数 bool lengthCompare(const string &, const string &);,函数指针的声明形式为:bool (*pf)(const string &, const string &);。有以下几点需要注意:

  • 当我们将函数名作为一个值使用时,该函数自动转换成指针:pf = lengthCompare;,当然使用取地址运算符也是允许的:pf = &lengthCompare;,这两种复制方式等价;
  • 调用函数可以直接使用函数指针,不需要解引用:bool b1 = pf("hello", "goodbye");,当然解引用也是允许的:bool b2 = (*pf)("hello", "goodbye");,这两种调用方式等价;
  • 在指向不同函数类型的指针间不存在转换规则,但是可以为函数指针赋一个 nullptr 值,或者值为 0 的常量表达式,表示该指针没有指向任何函数。
  • 当使用重载函数的时候,上下文必须清楚界定到底是使用了哪个函数,指针类型必须与重载函数中的某一个精确匹配,例子如下:
    1
    2
    3
    4
    void ff(int *);
    void ff(unsigned int);
    void (*pf1)(int) = ff;          // error: no matching function for call to 'ff'
    void (*pf2)(int *) = ff;        // ok
    
  • 虽然不能定义函数类型的形参,但是形参可以是函数指针,这时,虽然形参看起来是函数类型,但是实际上却当成是函数指针类型使用,下面两个声明是等价的:
    void useBigger(const string &, const string &, bool pf(const string &, const string &));
    void useBigger(const string &, const string &, bool (*pf)(const string &, const string &));
    
    我们可以直接将函数作为实参使用,其将自动转换成指针:useBigger(s1, s2, lengthCompare);
  • 函数指针也可以作为返回值,但是我们必须将返回类型写成指针形式,编译器不会自动将函数转换成指针,一般有下边三种写法:
    • 使用类型别名:using F = int(int *, int); F *f1(int);,这里定义的 F 是函数类型,函数不能直接返回函数类型,还可以使用 using PF = int(*)(int *, int); PF f1(int);
    • 直接强行声明:int (*f1(int))(int *, int);,这里要从里往外读:f1 有形参列表,所以是个函数,并且应该返回一个指针,这个指针的类型也包括形参列表,所以是一个函数指针;
    • 尾置返回类型:auto f1(int) -> int (*)(int *, int);,这种写法更加直观。
  • typedefdecltype 和上面的 using 一样,也区分函数类型与函数指针类型:
    1
    2
    3
    4
    typedef decltype(lengthCompare) Func1;                   // Func1 是函数类型
    typedef bool Func2(const string &, const string &);      // Func2 是函数类型
    typedef decltype(lengthCompare) *Func3;                  // Func3 是函数指针类型    
    typedef bool (*Func4)(const string &, const string &);   // Func4 是函数指针类型