【C/C++】内存基础

1. 基本数据类型

short s = 0x4142;    // 16进制
char c = *(char*)&s;
cout << c << endl;

我的电脑上输出为字符 'B'

Why???

short 型在内存中占 2 字节(bytes),bit 表示如下。

&s 取 s 的地址,(char *)&s 让机器强行认为该指针指向 char 型元素,而 char 型在内存中占 1 字节。

因此用 * 取地址后只向后获取 8 bits(1 字节),得到 0x41 赋值给 char 型变量 c。

但 0x41 = 65,这样 char c 应该是字符 'A' 啊???

因为计算机系统以字节(8 位)为单位,对于位数大于 8 位的处理器,例如 16 位或 32 位处理器,由于寄存器宽度大于 1 字节,那么必然存在着安排多个字节的问题。因此就导致了大端存储模式和小端存储模式,俗称大尾 / 小尾。

大尾,是指数据的低位(权值较小的后面那几位)保存在内存的高地址(图示中从左到右即为内存地址从低到高)中;而数据的高位,保存在内存的低地址中。

而我的电脑是小尾处理器,即 short s 的低位 0x42 保存在内存的低地址,高位 0x41 保存在内存的高地址。也就是说,

((char*)&s)[0] = 0x42  // 低地址单元

((char*)&s)[1] = 0x41  // 高地址单元

因此我的电脑将 0x42 赋值给 c,因此输出 'B'。

 

2. 结构体

struct st {
    int a;
    int b;
};

st s;
s.a = 1;
s.b = 2;
cout << s.b << endl;
((st*)&(s.b))->a = 99;
// ((st*)&(s.b))->b = 99;    // 很可能报错,操作不合法内存
cout << s.b << endl;

输出为

2

99

Why 99???

 

而 ((st*)&(s.b))->b 这片内存很可能未开辟,修改该处的值很可能报错。

 

结构体的大小(sizeof)需要考虑存储结构体变量时的地址对齐问题。

例如以下 2 个结构体

 

struct Struct1 {
    char a;
    int n;
    char b;
};

struct Struct2 {
    char a;
    char b;
    int n;
};

 

用 sizeof() 求这两个结构体大小,Struct1 是 12,Struct2 是 8,WHY???

  结构体变量大小 = 最后一个成员变量的地址 + 最后一个成员变量的大小

  Struct1 中第一个成员变量的地址就是结构体变量的首地址,第一个成员 char a 的偏移地址为 0;第二个成员 int n 的地址是第一个成员的地址加上第一个成员的大小(0 + 1),其值为 1;第三个成员 char b 的地址是第二个成员的地址加上第二个成员的大小(1 + 4),其值为 5。

  然而,实际存储结构体变量时地址要求对齐,编译器在编译程序时有自己的规则,大多编译器会遵循以下两条原则:

(1)结构体中成员变量的偏移地址 = 其自身大小的整数倍 

(2)结构体变量大小 = 所有成员变量大小的整数倍,也即所有成员变量大小的公倍数

  上例中 Struct1 的第二个成员变量 int n 的偏移地址为 1,并不是自身大小(4)的整数倍,因此编译器在处理时会在第一个成员后面补上 3 个空字节,使得第二个成员的偏移地址变成 4;这样第三个成员 char b 的偏移地址就是(4 + 4),其值为 8;最后结构体变量的大小等于最后一个成员的偏移地址加上其大小(8 + 1),其值为 9,不是所有成员变量大小的整数倍(9 不是 4 的整数倍),因此编译器会在第三个成员变量 char b 后面补上 3 个空字节,结构体总大小即为 12,满足要求。Struct2 也是同理,会在第二个成员 char b 后补 2 个空字节以满足要求。

  通过输出这两个结构体每个成员的地址,会更加清楚这种地址结构。

 

int main() {
    Struct1 s1;
    Struct2 s2;
    cout << sizeof(s1) << " " << sizeof(s2)  << endl;
    cout << "Struct1:" << (void*)&s1 << endl
         << "a: " << (void*)&(s1.a) << endl
         << "n: " << (void*)&(s1.n) << endl
         << "b: " << (void*)&(s1.b) << endl;
    cout << "Struct2:" << (void*)&s2 << endl
         << "a: " << (void*)&(s2.a) << endl
         << "n: " << (void*)&(s2.b) << endl
         << "b: " << (void*)&(s2.n) << endl;
    return 0;
}

输出结果为

 

嵌套结构体的大小(sizeof)需要将其展开考虑。 原则如下:

(1)展开后的结构体的第一个成员变量的偏移地址 = 被展开的结构体中最大的成员变量大小的整数倍。

(2)整个结构体变量大小 = 所有成员大小的整数倍(所有成员计算的是展开后的成员,而不是将嵌套的结构体当做一个整体)。

 

3. Union

Union 是一种特殊的结构体。它能够包含访问权限(默认访问权限是 public)、成员变量、成员函数(可以包含构造函数和析构函数),但不能包含虚函数、静态数据变量、引用(后两者无法共享内存),也不能被用作其他类的基类,它本身也不能从某个基类派生而来。

Union 类型的成员之间是共享内存的,同一时刻,一个 Union 中只有一个值是有效的。因此当多种数据类型要占用同一片内存时,即“n 选 1”时,可以使用 Union 来发挥其长处。

如下例 Union

union Union {
    struct StructInUnion {
        int a;
        int b;
    }s;
    int c;
    int d;
};

观察对不同成员赋值造成的相互影响s、a、c、d 的首地址相同

 1 int main() {
 2     Union u;
 3     u.s.a = 1;
 4     u.s.b = 2;
 5     cout << "a: " << u.s.a << endl
 6          << "b: " << u.s.b << endl
 7          << "c: " << u.c << endl
 8          << "d: " << u.d << endl;
 9     cout << "----------" << endl;
10     u.c = 3;
11     cout << "a: " << u.s.a << endl
12          << "b: " << u.s.b << endl
13          << "c: " << u.c << endl
14          << "d: " << u.d << endl;
15     cout << "----------" << endl;
16     u.d = 4;
17     cout << "a: " << u.s.a << endl
18          << "b: " << u.s.b << endl
19          << "c: " << u.c << endl
20          << "d: " << u.d << endl;
21     return 0;
22 }            

输出结果

 

3. 数组

主要地,array + k = &array[k],数组即是数组首元素的地址。

类似地,可以通过 k 合法 / 不合法地操作内存。

 

4. swap() 函数

实现交换两个元素内容的函数swap,对于不同数据类型,C++可使用模板 template,使用模板更加的类型安全(type safe)。

但其实,当编译器发现模板函数被使用(注意,不是被定义),则在编译这段代码时会使用那个强类型构造一个新函数,导致代码膨胀,因此编译效率并不高。

此时可以通过无类型指针 void* 实现泛型编程,优点是执行速度很快,只需要一份代码副本。

 

但是, 

void swap(void *vp1, void *vp2) { 
    void temp = *vp1; 
    *vp1 = *vp2; 
    *vp2 = temp;
}

这样写是错误的!!!因为

  • 变量无法声明为 void 类型
  • void* 无法被解引用,因为系统没有此地址指向的对象的大小信息

 

要想实现泛型函数,需要在调用的地方传入相关要交换的对象的地址空间大小 size。

void swap(void *vp1, void *vp2, int size) {
    char *buffer = new char[size];
    memcpy(buffer, vp1, size);
    memcpy(vp1, vp2, size);
    memcpy(vp2, buffer, size);
    delete[] buffer;
}

具体使用如下

int x = 11, y = 22;
cout << "x = " << x << ", y = " << y << endl;
swap(&x, &y, sizeof(int));
cout << "x = " << x << ", y = " << y << endl;
char *a = strdup("storm"), *b = strdup("nevermore");
cout << "a = " << a << ", b = " << b << endl;
swap(&a, &b, sizeof(char **));    // 字符串不等长,需要交换指针
cout << "a = " << a << ", b = " << b << endl;
swap(a, b, sizeof(char *));        // 错误!!!注意与上面的区别
cout << "a = " << a << ", b = " << b << endl;

 注意输出结果的区别

 

注意字符串的交换传递的参数是二级指针 &a 和 &b,如图

如果直接将一级指针 a、b 作为参数传递,则 swap 函数执行过程中 vp1 的内容是字符串 “storm\0” 的地址,mencpy 后缓冲区 buffer 指向的内容的将是字符串 “stor”(指针变量一律占 4 字节),因此最后交换的是两个字符串的前 4 个字节。

PS:数组名 a、b 即是一级指针,a = &a[0] 隐式传入 &。

 

5. 线性搜索函数

int 型版本 

int lsearch(int key, int array[], int size) {
    for (int i = 0; i < size; i++)
        if (array[i] == key)
            return i;
    return -1;
}

 

基本数据类型数组的泛型线性搜索函数的实现

// key为搜索值的指针,base为基数组的指针,n为数组元素个数,elemSize为每个元素的大小
void* lsearch(void *key, void *base, int n, int elemSize) {
    for (int i = 0; i < n; ++i) {
        void *elemAddr = (char*)base + i * elemSize;
        if (memcmp(key, elemAddr, elemSize) == 0)
            return elemAddr;
    }
    return NULL;
}

TIP: 代码第 3 行,将基数组的首地址强制转换为 char 型指针。WHY???

对于非 void 指针,如 int array[10];

array[i] 等价于 *(array + i),编译器会根据数组类型在 i 后面乘以 sizeof(int),以获取正确的内存地址。

不允许对 void 指针进行算术运算,是因为编译器不知道 void 数组中每个元素的大小,仅仅使用 base + i 的话编译器并不知道 base 数组中每个元素大小为 elemSize。

这里的强制类型转换利用了 char 型大小为 1 字节的特性,使 elemAddr 指向此 void 数组的第 i 个元素的首地址。

 

使用 memcmp 可以比较 char、int、double 等基本数据类型,但无法比较字符指针 char * 类型,因为 char * 所指向的字符串不等长, memcmp 的第三个参数无法确定。也就无法在 char ** 字符指针数组中找到所要的 char * 字符串。

因此可以通过函数指针传入我们自定义的比较函数。

// 为char**定制的比较函数
int StrCmp(void *vp1, void *vp2) {
    char *s1 = *(char **)vp1;
    char *s2 = *(char **)vp2;
    return strcmp(s1, s2);
}

void* lsearch(void *key, void *base, int n, int elemSize, int (*cmpfn)(void *, void *)) {
    for (int i = 0; i < n; i++) {
        void *elemAddr = (char *)base + i * elemSize;
        if (cmpfn(key, elemAddr) == 0)
            return elemAddr;
    }
    return NULL;
}

函数的使用如下

 

int main() {
    char *heroes[] = {"JUGG", "QOP", "Storm Spirit", "Zeus", "SF"};
    char *a1 = "JUGG", *a2 = "yyf";
    char **found;
    found = (char **)lsearch(&a1, heroes, 5, sizeof(char *), StrCmp);
    if (found)
        cout << *found << " is Found!" << endl;
    else
        cout << "Not Found!" << endl;
    found = (char **)lsearch(&a2, heroes, 5, sizeof(char *), StrCmp);
    if (found)
        cout << *found << " is Found!" << endl;
    else
        cout << "Not Found!" << endl;
    return 0;
}

 

 

 

输出为

JUGG is Found!

Not Found!

 

注意:

  • heroes 的类型是 char** ,即字符指针数组,数组元素都是指向一个字符串的指针,这些字符串不在堆(heap)中,它们是字符串常量,存储在静态存储区。(C语言中没有字符串变量,只能用字符数组 char [] 表示)
  • 由于 base 是字符串指针数组,即 char**,所以返回值 elemAddr 也是 char** 类型,所以将 found 设置为 char** 类型。当 found 不为空,对 found 解引用后,输出的 *found 为字符指针 char* 类型,指向所要找的字符串。
  • 为什么给 char** 定制的比较函数 StrCmp 要这样写???
    • 我们逻辑上知道 elemAddr 是 char** 类型,并且我们希望对 vp1 和 vp2 的处理保持格式一致,因此我们传递的参数key,即 &a1 和 &a2 也是 char** 类型。因此把 vp1 和 vp2 强制类型转换为 char**,这样在逻辑上正确。
    • 我们继续将 vp1 和 vp2 解引用,是因为这样得到的 s1 和 s2 是 char* 类型,对字符指针指向的字符串比较可以调用内置函数 strcmp()。

 

以上。

转载于:https://www.cnblogs.com/wayne793377164/p/8796471.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值