C++基础杂记(1)

发布时间 2023-10-31 13:58:51作者: Torch_HXM


结构体中的位字段

struct torgle_register
{
    unsigned int SN : 4;    // 为SN分配四位空间
    unsigned int : 4;       // 设置四位空间空余
    bool goodIn : 1;        // 为goodIn分配一位空间
    bool goodTorgle : 1;    // 为goodTorgle分配一位空间
};

torgle_register tr = {14, true, false};

共用体

union one4all           // 共用体的大小为其中最大数据类型的大小
{
    int int_val;
    long long_val;
    double double_val;
};

one4all pail;           // 创建对象
pail.int_val = 15;      // 此时,共用体pail中储存的值为 int 15
pail.double_val = 1.38; // 此时,int_val的值被抛弃,共用体pail中储存的值为 double 1.38

烦人的枚举

枚举的声明与赋值

enum spectrum {red, orange, yellow};                // 定义了一个枚举类型spectrum,他包含的元素为red, orange和yellow。默认情况下red=0, orange=1, yellow=2,依次递增
enum spectrum {red, blue = 0, orange = 100, yellow};// 定义了一个枚举类型spectrum,他包含的元素为red, blue, orange和yellow。其中red=0, blue=0, orange=100, yellow=101
spectrum band;                                      // 声明了一个spectrum枚举类型的枚举变量
band = red;                                         // 给枚举变量band赋值为red。枚举变量的值只能为其枚举类型的一个元素

枚举的取值范围与强制类型转换

enum spectrum {red, blue = 0, orange = 100, yellow};// 定义了一个枚举类型,他的值可以是red, blue, orange或yellow。其中red=0, blue=0, orange=100, yellow=101
spectrum band;                                      // 声明了一个枚举变量
band = spectrum(6);                                 // 将6强制转换为枚举变量并赋值给band。

虽然枚举变量储存的值是整数,但是,直接将整数赋给枚举变量是不合法的,枚举变量只能接受声明他的枚举类型所包括的元素作为其值。特殊情况下,可以将枚举类型取值范围内的整形强制转换为该枚举类型的元素来赋给该枚举类型的枚举变量。一个枚举类型的取值范围如下:如果该枚举类型的最大元素值为\(n\),则该枚举类型取值范围的上限\(2^N-1\),其中 \(2^n \le 2^N \le 2^{n+1}\)\(N\) 为整数;设该枚举类型的最小元素值为 \(n\),当 \(n \ge 0\) 时,该枚举类型取值范围的下限为 0,当 \(n < 0\) 时,则该枚举类型取值范围的下限\(2^N+1\),其中 \(2^n \ge 2^N \ge 2^{n+1}\)\(N\) 为整数。

枚举的注意事项

  • 整形不能够直接赋值给枚举变量。在枚举类型取值范围内的整形可以通过强制转换赋值给枚举变量。
  • 枚举变量参与运算时会被强制转换为int。

指针

为什么是 int* ptr 而不是 int *ptr ?

为什么C++程序员在定义指针时习惯写成int* ptr 而不是 int *ptr ?,看下面一个例子:

#include <iostream>
using namespace std;
int main(int argc, char** argv)
{
    int int_value = 5;
    int* ptr_value = &int_value;

    cout<< "int_value: "<< int_value<< " 地址:"<< &int_value<< endl;
    cout<< "ptr_value: "<< ptr_value<< " 值:"<< *ptr_value<< endl;

    return 0;
}

int_value: 5 地址:0x7fff00e5116c
ptr_value: 0x7fff00e5116c 值:5

先说一下二者的区别:如果写成int *ptr,则认为*ptr是一个整体,定义了一个整形;若果写成 int* ptr,则是认为int*是一种变量类型,这个变量类型定义了一个指针。当从示例来看,地址&int_value并没有赋给*ptr_value而是赋给了ptr_value,这代表 int*ptr 定义了一个指针而不是一个值,因此写成int* ptr更合适。另外,这样理解的好处还体现在指针安全上。

避免危险的指针

常见的危险指针的例子:

int* ptr;
*ptr = 100;

乍一看上去好像没问题,但是,int* ptr并没有指明初始化的地址,这将导致 ptr 被赋予随机地址,而100将被储存在这个随即地址中,这将导致原有的内存内容被破坏,会给我们的程序造成危险。正确的做法如下:

int* ptr = new int; // 为指针分配内存
*ptr = 100;
delete ptr;         // 不删除ptr变量本身,但释放掉new给他的内存

另外,数组在声明时会被分配内存,下列代码不会造成野指针:

int arr[3];

使用array和vector来避免数组的反向越界

C++提供了四种数组:

  • 标准数组 int a[size]。储存在栈中。
  • array数组 array<int size> a。储存在栈中。
  • vector动态数组 vector<int> a。储存在自由存储区域或堆中。
  • valarray数组 valarray v(size)。 valarray 是一个模板类,专门用来处理数值数组。它为数值数组提供加和、找最大值、找最小值等算数操作。他的构造函数(初始化方法)有很多:
    int gpa[] = {1, 2, 3, 4, 5};
    
    valarray<int> v1;         // 一个 int 类型的数组,不包含元素
    valarray<int> v2(8);      // 一个 int 类型的数组,包含 8 个元素
    valarray<int> v3(10, 8);  // 一个 int 类型的数组,包含 8 个元素,都初始化为 10
    valarray<int> v4(gpa, 4); // 一个 int 类型的数组,包含 4 个元素,初始化为 gpa 的前四个元素
    

当数组长度固定时,使用array而不是标准数组会给我们提供便利。 在C++中,标准数组的下列语句是可以通过编译的:

int a[] = {1, 2, 3, 4};
a[-2] = 10;             // 虽然已经越界了,但是仍然会将*(a-2)的位置赋值为10

而array数组的下列语句将会报错:

array<int 4> a = {1, 2, 3, 4};
a.at(-2) = 10;          // at会对越界进行检查

terminate called after throwing an instance of 'std::out_of_range'
what(): array::at: __n (which is 18446744073709551614) >= _Nm (which is 4)
Aborted

vector和array有相同的用法。

++x 和 x++

int x = 5;
int y = ++x;    // x=6, y=6; ++x返回值为(x+1)

int x = 5;
int y = x++;    // x=6, y=5; x++返回值为(x)

另外,x++在运行时会先记录x的值用于返回,在将x+1;而++x将x+1后直接返回。当使用for循环时,从效率上看,++x比x++更好。

判断 C 风格字符串是否相同为什么不能直接用 ==

char* s[] = "mate";
bool bequal = (s=="mate");

上述表达式从表面上看似乎在判断字符串s是不是"mate"。但实际上,由于s不是字符串本身,而是字符串数组的地址,"mate"也不是字符串本身,而是这个字符串常量的地址,所以表达式s=="mate"实际上是在判断字符串变量s与字符串常量"mate"的地址是否相同,所以bequal的结果是false。

在C++中双引号"..."字符串表示的是存储该字符串的地址,而单引号'.'字符表示的是字符本身,且由于字符实际上是一个整型,所以字符本身也是整形本身。

虽然C风格的字符串无法使用==来判断相等,但是C++风格的字符串(由string类提供)可以。

typedef 与 define

  • #define name value 是预处理命令,会机械的将代码中的 name 替换为 value,不做正确性检查。没有作用域限制。
  • typedef value name; 是关键字,会将代码中的 name 替换为 value,用来定义类型的别名,因此在为类型取别名时 typedef 有优于 define 的特性。typedef 有自己的作用域。

typedef 在定义类型别名上的优势参见如下例子:

#define intptr1 int*
typedef int* intptr2;

intptr1 p1, p2;
intptr2 p3, p4;

在该代码中 p1, p3, p4 是整形指针,而 p2 是整形。可以看出,当使用 #define 给类型取别名时, 由于是机械的替换程序中的别名,造成无法对指针变量进行连续的定义;而 typedef 不存在这样的问题。

另外,在使用 typedef 和 define 时要注意,const 的使用位置不同代表不同的意思,参考如下示例程序:

#define INTPTR1 int*
typedef int* INTPTR2;

int a = 1;
int b = 2;
int c = 3;

const INTPTR1 p1 = &a;
const INTPTR2 p2 = &b;
INTPTR2 const p3 = &c;

首先说明两个重要概念:

  • 指针常量:指针类型的常量。例如 int* const p,指针 p 代表的地址是无法被改变的,但是指针 p 所指向的值可以变。也就是说,指针 p 本身是一个常量。
  • 常量指针:指向常量的指针。例如 const int* p,指针 p 代表的地址是可变的,但是所指向的值无法被改变。也就是说,值 *p 本身是一个常量。这里的无法被改变是指无法使用 *p = value 语句更改该地址指向的值,但由于 p 是可以改变的,所以 *p 会因为 p 的不同而不同。

在示例程序中,p1 是常量指针,可以使用如下程序验证:

#include <iostream>
using namespace std;

#define intptr1 int*
typedef int* intptr2;

int main(int argc, char** argv)
{
    int a = 1;
    int b = 2;

    const intptr1 p1 = &a;

    p1 = &b;

    *p1 = 10;

    cout<< *p1<< endl;

    return 0;
}

test.cpp: In function ‘int main(int, char**)’:
test.cpp:17:8: error: assignment of read-only location ‘* p1’
*p1 = 10;
^~

程序在编译时报错,*p1 是只读的,不可以被改变。但是代码中改变指针 p1 的语句 p1=&b 是合法的,说明指针自身可以被改变。因此 p1 是常量指针。

在示例程序中,p2 为指针常量,可以使用如下程序验证:

#include <iostream>
using namespace std;

#define intptr1 int*
typedef int* intptr2;

int main(int argc, char** argv)
{
        int a = 1;
        int b = 2;

        const intptr2 p2 = &a;

        *p2 = 10;

        p2 = &b;

        cout<< *p2<< endl;

        return 0;
}

test.cpp: In function ‘int main(int, char**)’:
test.cpp:16:8: error: assignment of read-only variable ‘p2’
p2 = &b;
^

程序在编译时报错,p2 是只读的,不可以被改变。但是代码中改变值 *p2 的语句 *p2=10 是合法的,说明指针指向的值可以被改变。因此 p2 是指针常量。

在示例程序中, p3 也是指针常量。这里不再设计程序验证。

可以看到,typedef 关键字将指针类型(例如 int* )组合为一个整体,并定义别名,这个特质让示例程序中的const INTPTR2 p2 = &b 和 INTPTR2 const p3 = &c产生了一致的结果,都是定义了一个指针常量。因此,在为类型定义别名时,最好使用 typedef ,具有更好的可解释性。

C++11 for循环的新特性

int prices[] = {1, 2, 3, 4, 5};
for(int x : prices)
{
    cout<< x<< endl;
}

这段代码会将数组 prices 中的值从头开始以此以传值的方式传给 x,并由 cout 输出。在循环体中修改 x 无法改变数组 prices 中的值。

int prices[] = {1, 2, 3, 4, 5};
for(int &x : prices)
{
    cout<< x<< endl;
}

这段代码会将数组 prices 中的值从头开始以此以传引用的方式传给 x,并由 cout 输出。在循环体中修改 x 会改变数组 prices 中的值。

声明一个与表达式类型相同的变量

decltype(x*n) y;    // y 的类型将与 x*n 相同

函数返回类型声明

auto func1()->double;
auto func2()->decltype(x*n);

自动释放内存的智能指针

  • auto_ptr
  • unique_ptr
  • shared_ptr
  • weak_ptr