【现代C++】第二章 类型

1 基本类型

1.1 整数类型

类型有无符号64位Windows系统下的大小64位Linux/Mac系统下的大小prinf格式指定符字面量后缀
shortY22%hd
unsigned shortN22%hu
intY44%d
unsigned intN44%u
longY48%ld[L]
unsigned longN48%lu[UL]
long longY88%lldL or LL
unsigned long longN88%lluUL or ULL
  • 如果想确保整数的大小,可以使用<cstdint>库中的整数类型,如int16_tint32_tint64_t
  • 整数字面量中可以包含任意数量的单引号(')以方便阅读,例如1'000'000'000。python 中的下划线符号_也有相似的用途,例如1_000_000_000

字面量是程序中的硬编码值,可以使用以下四种硬编码的整数字面量:

进制数字面量前缀printf格式指定符
20b%b
80%o
10默认默认
160x%x

1.2 浮点类型

  • 浮点数在 C++ 中有三种精度级别:
    • float——单精度
    • double——双精度
    • long double——扩展精度
  • 浮点数字面量一般被认为是双精度,需要指定时,则使用后缀:
    • 单精度:F
    • 扩展精度:L
  • 字面量也可以使用科学计数法,例如1.123e-34。其中基数和指数之间不能有空格
  • 格式指定符%f显示带有小数位的浮点数,%e则使用科学计数法表示,而%g选择前两者中更加紧凑的那一个
  • 对于扩展精度,格式指定符可以写为%lf%le%lg,不过在实践中可以省略,因为printf会自动将浮点数提升为双精度类型

1.3 字符类型

  • C++中的字符类型主要存储人类可以读懂的信息。一共有六种字符类型:
    • char:默认的字符类型,总是1字节
    • char16_t:用于 2 字节的字符集(如 UTF-16)
    • char32_t:用于 4 字节的字符集(如 UTF-32)
    • signed char:与char相同,但总是有符号的
    • unsigned char:与char相同,但总是无符号的
    • wchar_t:足够大以至于可以包含实现平台地区环境语言设置中的最大字符
  • 所有的字符都使用单引号''括起来,若字面量想要指定为char之外的其他类型,则需要提供前缀:
    • L代表wchar_t
    • u代表char16_t
    • U代表char32_t
  • 有些字符在屏幕上不显示,或如引号之类有特殊用途的字符,可以使用转移序列,如\"(双引号)和\n(换行)
  • 对于 Unicode 字符,有两种表示方法
    • 在前缀\u后面加上4位 Unicode 码位
    • 在前缀\U后面加上8位 Unicode 码位

1.4 bool类型

  • bool类型的字面量有true或者false
  • 整数类型和bool类型可以互相追换,1转换为true0转换为false
  • bool类型没有格式指定符,但可以使用%d将其以整数形式输出,这是因为printf会将所有小于int的整数值提升为int

1.5 std::byte类型

  • std::byte定义在<cstddef>头文件中,允许按位逻辑运算
  • std被称为命名空间

1.6 size_t类型

  • size_t也被定义在<cstddef>头文件中,用来表示对象的大小
  • size_t保证其大小足以表示对象的最大字节数
  • 在实践中,通常与架构系统中的unsigned long long相同
  • 一元运算符sizeof接受一个对象,返回该类型的大小
  • size_t的格式指定符通常为%zd(十进制)和%zx(十六进制),方便在打印结果中以这两个进制数查看对象的大小

1.7 void类型

void类型表示一个空的值集合。我们只有在函数不返回值时使用void。因为void对象不拥有值,故C++不允许使用void对象。

2 数组

2.1 初始化

有以下两种初始化方法

int my_array[100];
int my_array_[] = { 1, 2, 3, 4, 5 }; // 编译时会自动推断数组的长度

2.2 访问数组中的元素

我们使用arr[i]这样的语法访问数组中索引为i的元组,注意在 C++ 中数组的元素索引是从0开始的

2.3 for循环简介

for循环可以重复运行一些语句特定次数。可以规定一个循环启点和其他条件。其结构如下

for(init_statement; conditional; iteration_statement)
{
    // ...
}

例如,以下代码可以在给定数组中寻找最大值:

#include<cstddef>
#include<cstdio>

int main()
{
    unsigned long maximum = 0;
    unsigned long values[] = { 10, 50, 20, 40, 0};
    for(size_t i = 0; i < 5; i++)
    {
        if(values[i] > maximum)
        {
            maximum = values[i];
        }
    }
    printf("The maximum value is %lu", maximum);
}
  • 需要注意的是,在这里的循环结构中,我们将循环变量i的类型定义为size_t,是因为values可能占用最大可用存储,这样使用size_t可以保证对数组中的任何值进行索引,在实践中,size_tint没有区别,但前者在技术上是正确的

2.3.1 基于范围的for循环

我们可以使用基于范围的for循环来消除迭代器变量i

for(element_type elemrnt_name : array_name)
{
    // ...
}
  • 上述模版中的element_type必须要和数组中元素的类型一致

2.3.2 数组的长度

可以使用这样的偏技术的代码计算数组的长度:

short array[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
size_t n_elements = sizeof(array) / sizeof(short);
  • 本方法过于偏重技巧,但广泛应用于旧代码中
  • 如果必须使用数组,则可以使用<iterator>头文件中的std::size函数安全的获取元素的数量
  • 需要注意的是,std::size可以作用在任何暴露了size方法的容器上,在编写泛型时非常有用,这种容器可以被认为是一种鸭子类型

2.4 C风格字符串

  • C 风格字符串和以 null 结尾的字符串会在末尾添加一个\0,以表示字符串的结束
  • 因为数组元素是连续的,我们可以在字符类型的数组中存储字符串

2.4.1 字符串字面量

  • 引号""括住的文本可声明为字符串的字面量
  • 字符串字面量支持 Unicode

2.4.2 格式指定符

  • 窄字符串char*的格式指定符是%s
  • 将 Unicode 打印至控制台比较复杂,我们需要确保选择了正确的代码页。若想将 Unicode 嵌入字符串字面量,可以参考<cwchar>头文件中的wprintf
  • 连续字符串的字词会被串在一起,任何中间的空白和换行会被忽略,因此可以将字符串字面量分多行放置
char house[] = "a "
               "house "
               "of "
               "gold.";

2.4.3 ASCII

  • ASCII 是美国信息交换标准代码的简称
  • 其中含有控制代码和可见的字符

下面给出一个将上面几个元素结合的例子

#include<cstdio>

int main()
{
    char alphabet[27];
    alphabet[26] = 0;

    // 小写字母
    for(int i = 0; i < 26; i++)
    {
        alphabetp[i] = i + 97;
    }
    printf("%s\n", alphabet);
    
    // 大写字母
    for(int i = 0; i < 26; i++)
    {
        alphabetp[i] = i + 65;
    }
    printf("%s\n", alphabet);
}

3 用户自定义类型

用户自定义类型有三大类:

  • 枚举类型:最简单的用户自定义类型,枚举类型可取的值限定在列出的一组可能值中
  • :功能更前面的类型,可以灵活地结合数据和函数。只含有数据的类型被称为普通数据类 (Plain-Old-Data, POD)
  • 联合体:浓缩的用户自定义类型,所有成员共享一个内存位置。其本身十分危险,不可滥用

3.1 枚举

  • 使用关键字enum class声明枚举类型
  • 关键字后面是可以取的类型名称和它可以取的值的列表
  • 这些值是任意的字母-数字字符串,在实现内部,它们只是整数;但它们允许使用程序员自定义的类型来指定,因此更加安全

例如,这里声明了一个包含部分编程语言的枚举类:

enum class ProgramLang
{
    C;
    CPP;
    Java;
    Python;
    Julia;
}; // 别忘了这个分号

注意,这里的enum class只是枚举类型的一种,被称为作用域枚举。为了和 C 语言兼容,C++也支持非作用域枚举类型,这种类型使用enum关键字声明。二者的主要区别是作用域枚举类型需要在值前面加上枚举类型和作用域运算符::,而非作用域枚举不需要。这代表着非作用域枚举在使用时没有加作用域枚举安全,如果非必要不要使用非作用域枚举

3.1.1 switch语句

switch语句的结构如下所示:

switch (condition)
{
    case (case_a)
    {
        // ....
    } break;
    case (case_b)
    {
        // ...    
    } break;
    //  more cases ...
    default
    {
        // handle detault case 
    }
}
  • switch语句根据条件,将控制权转交给几个代码块中的一个进行接下来的运行操作
  • 如果所有条件均不满足,则执行默认条件的语句块(default
  • switch的运行将持续至switch语句结束或碰见break关键字。注意,这一点和 Java 中的情况相反

3.1.2 对枚举类型使用switch语句

下面是一个switch语句的例子,根据对应语言,输出其中打印"Hello world!"的语句

#include<cstdio>

enum class ProgramLang
{
    // ......
};

int main()
{
    ProgramLang lang = ProgramLang::Python;

    switch (lang)
    {
        case(ProgramLang::C)
        {
            printf("printf(\"Hello world!\");");
        } break;
        case(ProgramLang::CPP)
        {
            printf("cout << \"Hello world!\";");
        } break;
        case(ProgramLang::Java)
        {
            printf("System.out.println(\"Hello world!\");");
        } break;
        case(ProgramLang::Python)
        {
            printf("print(\"Hello world!\")");
        } break;
        case(ProgramLang::Julia)
        {
            printf("println(\"Hello world!\")");
        } break;
        default
        {
            printf("Hello world!");
        }
    }
}
  • 每个case的大括号可有可无,但强烈推荐使用
  • default语句是一个安全功能,这保证有新的项目添加到枚举类中时运行时将检测到这个未知的条目

3.2 普通数据类

  • POD是一个最简单的容器,可以看做是一种潜在的不同数据类的异构数组,类的元素叫做成员
  • 每个 POD 都以struct关键字开头,后面跟着 POD 的名称

下面是一个 POD 的例子

struct Book
{
    char name[255];
    int year;
    int pages;
    bool hardcover;
}

声明 POD 变量和声明其他变量一样,我们可以使用点运算符.访问其中的成员变量:

#include<cstdio>

struct Book
{
    // ...
};

int main()
{
    Book neorumanceer;
    neorumanceer.pages = 271;
    print("Neorumanceer has %d pages.", neorumanceer.pages);
}
  • POD 可以与 C 语言兼容,可以使用高效的机器指令复制或移动,且可以在内存中有效地表示
  • C++ 保证成员在内存中按顺序排列,尽管有些实现要求成员沿着字边界对齐,这取决于 CPU 的寄存器长度
  • 一般而言,应该在 POD 定义中从大到小排列成员

3.3 联合体

  • 联合体类似于 POD,它把全部的成员放在一起
  • 可以将联合体看做对内存块的不同看法和解释
  • 在一些底层情况下,例如处理必须在不容架构下保持一致的结构、处理和 C/C++ 互操有关的类型检查问题时、在包装位域 (bitfield) 时
  • 在声明联合体时,只需将关键词从struct换成union即可,也使用.运算符来指定联合体的解释
  • 从语法上看,联合体和 POD 并没有什么两样,但其内部完全不同
  • 联合体的所有成员都用在同一个地方,于是很容易造成数据损坏(看下面的例子);因此除了函件情况,应该避免使用联合体
#include<cstdio>

union Variant
{
    char string[10];
    int integer;
    double floating_point;
};

int main()
{
    Variant v;
    v.integer = 42;
    print("The ultimate integer: \t%d", v.integer);
    v.floating_point = 2.7182818284;
    print("Euler's Number e: \t\t%d", v.floating_point);
    print("A dumpster fire: \t\t%d", v.integer);
}

// Output
// The ultimate integer            :42
// Euler's Number e                :-2147483648
// A dumpster fire                 :-2147483648

4 全功能的 C++ 类

C++中,向类定义中添加方法访问控制即可实现封装

4.1 方法

  • 方法就是成员函数
  • 方法可以访问类中的所有成员
struct ClockOfTheLongNow
{
    int year;
    void add_year()
    {
		year++;
    }
};

4.2 访问控制

  • 访问控制可以限制类成员的访问
  • 公有和私有是两个主要的访问控制
  • 任何人都可以访问公有成员,但只有类自身可以访问其私有成员
struct ClockOfTheLongNow{
    void add_year(){
        year++;
    }
    bool set_year(int new_year){
        if(new_year < 2019) return false;
        year = new_year;
        return true;
    }
    int get_year(){
        return year;
    }
    
private:
    int year;
};

4.2.1 关键字 class

  • 除了默认的访问控制之外,structclass关键字声明的类是一样的,因此下面的写法也是完全正确的
class ClockOfTheLongNow{
    int year;
   
public:
    void add_year(){
        year++;
    }
    bool set_year(int new_year){
        if(new_year < 2019) return false;
        year = new_year;
        return true;
    }
    int get_year(){
        return year;
    }
    
};

4.2.2 初始化成员

  • 我们可以通过类向外暴露的方法来对其成员进行初始化……
int main(){
    ClockOfTheLongNow clock;
    if(!clock.set_year(2018)){
        // wil fail
        clock.sert_year(2019);
    }
    clock.add_year();
    printf("year: %d", clock.get_year());
}
  • 但通过类构造函数会更好,它会初始化对象,并在其生命周期之初强制执行类不变量

4.3 构造函数

  • 构造函数可以接受任意数量的参数
  • 可以实现任意多的构造函数,只要其参数彼此类型不同
class ClockOfTheLongNow{
    int year;
    
public:
	ClockOfTheLongNow(){
        year = 2019;
    }
    
	ClockOfTheLongNow(int year_in){
		if(!set_year(year_in)){
            year = 2019;
        }
    }
}

int main(){
    ClockOfTheLongNow clock{ 2020 };  // 初始化语句
    printf("year: %d", clock.get_year());
}

4.4 初始化

4.4.1 将基本类型初始化为零

将基本类型初始化为零有以下几种方法:

int a = 0;		// initialized to 0
int b{};		// initialized to 0
int c = {};		// initialized to 0
int d;			// maybe initialized to 0
  • 使用大括号{}初始化变量的方法叫做大括号初始化
  • 无论对象的作用域如何,大括号初始化总是适用,而其他方法不总适用

4.4.2 将基本类型初始化为任意值

类似地,将基本类型初始化为任意值也有以下几种方法

int e = 42;			// 使用等号的初始化
int f{ 42 };		// 使用大括号初始化
int g = { 42 };		// 使用等号加大括号的初始化
int h(42);			// 使用小括号的初始化
  • 以上四种方法都保证变量被初始化为指定的值

4.4.3 初始化 POD

POD也可以通过上述类似的方法进行初始化:

#include<cstdint>

struct PodStruct{
    uint64_t a;
    char b[256];
    bool c;
};

int main(){
    PodStruct ini_pod_1{};						// 所有域初始化为零
    PodStruct ini_pod_2 = {};					// 所有域初始化为零
    	
    PodStruct ini_pod_3{ 42, "Hello" };			// a 和 b初始化为对应的值,c被初始化为零
	PodStruct ini_pod_4{ 42, "Hello", true };   // 所有域均被初始化为确定值
}
  • 不可以用PodStruct ini_pod_1 = 0;这样的语句来进行初始化,因为它再语言规则中被明确禁止了

4.4.4 将 POD 初始化为任意值

  • 使用大括号的初始化中,初始化列表从左到右的类型要和POD声明时从上到下的类型一致
  • 省略的字段不会被编译(例如PodStruct ini_pod_4{ 42, true };),且只能从右到左省略字段
  • 不能使用小括号初始化 POD

4.4.5 初始化数组

数组的初始化方法如下

int main(){
    int arr_1[]{ 1, 2, 3 };		// Length 3; 1, 2, 3
    int arr_2[5]{};				// Length 5; 0, 0, 0, 0, 0
    int arr_3[5]{ 1, 2, 3 };	// Length 5; 1, 2, 3, 0, 0
    int arr_4[5];				// Length 5; Uninitialized Values
}
  • 最后一个方法是否被初始化取决于初始化基本类型相同的规则

4.4.6 全功能类的初始化

  • 与基本类型和POD不同,全功能类在初始化时总是调用构造函数。具体使用哪一个构造函数取决于初始化时给出的参数
#include<cstdio>

class Taxonomist{
    Taxonomist(){
        printf("No Args");
    }
    Taxonomist(char x){
        printf("char: %c\n", x);
    }
    Taxonomist(int x){
        printf("int: %d\n", x);
    }
    Taxonomist(float x){
        printf("float: %f\n", x);
    }
}

int main(){
    Taxonomist t1;				// No args
    Taxonomist t2{ 'c' };		// char: c
    Taxonomist t3{ 65537 };		// int 65537
    Taxonomist t4{ 6.02e23f };	// float: 6.02.....
    Taxonomist t5('g');			// char: g
    Taxonomist t6 = { 'l' };	// char: l
    Taxonomist t7{};			// No args
    Taxonomist t8();			// 没有任何输出,将在以后详细解释 (Most vexing parse)
}

4.4.7 缩小转换

  • 在遭遇隐式缩小转换时,大括号初始化将会发出警告
float a{ 1 };
float b{ 2 };

int narrowed_resut(a/b);	// potentially nasty narrowing conversion
int result{ a/b };			// compiler generates a warning

4.4.8 初始化类成员

我们也可以使用大括号初始化来初始化类成员,但不可以使用小括号初始化成员变量

class JoanVanDerSmut{
    bool gold = true;
    int year_of_smelting_accident{ 1970 };
    chat ket_location[8] = { "x-rated" };
}

4.4.9 打起精神来

  • 在任何时候都使用大括号初始化方法
    • 大括号初始化方法几乎在所有地方都正常工作,引发的意外也最少
    • 大括号初始化也被称为”统一初始化“
  • 对于标准库中的某些类或者经手的项目,可能需要打破大括号初始化的规则

4.5 析构函数

  • 对象的析构函数是其清理函数
  • 析构函数在销毁对象前被调用
  • 析构函数几乎不会被明确的调用:编译器将确保每个对象的析构函数在恰当的时机被调用
  • 可以在类名之前加~符号来声明析构函数
#incude<cstdio>

class Earth{
    ~Earth(){
        printf("Making way for hyperspace bypass...");
    }
}
  • 析构函数不能接受任何参数
  • 如果不申明析构函数,则会生成默认的析构函数,后者什么都不做
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值