C/C++常见的易模糊知识点(结构体、指针、STL)

简介: C/C++常见的易模糊知识点(结构体、指针、STL)

语法

结构体对齐

三条规则

  • 结构体中元素是按照定义顺序一个一个放到内存中去的,但并不是紧密排列的。从结构体存储的首地址开始,每一个元素放置到内存中时,它都会认为内存是以它自己的大小来划分,因此元素放置的位置一定会在自己宽度的整数倍上开始(以结构体变量首地址为0计算)。
  • 在经过第一原则分析后,检查计算出的存储单元是否为所有元素中最宽的元素的长度的整数倍,是,则结束;若不是,则补齐为它的整数倍。
  • 结构体成员为结构体对象时需要着重理解

分别举例

1. 自己的大小来划分

2. 宽的元素的长度的整数倍

#include <iostream>
using namespace std;
struct X {
    char a;
    int b;
    double c;
}s1;
struct Y {
    char a;
    double b;
    int c;
}s2;
void print1() {
    cout << sizeof(s1) << " ";
    cout << sizeof(s1.a) << " ";
    cout << sizeof(s1.b) << " ";
    cout << sizeof(s1.c) << endl;    
}
void print2() {
    cout << sizeof(s2) << " ";
    cout << sizeof(s2.a) << " ";
    cout << sizeof(s2.b) << " ";
    cout << sizeof(s2.c) << endl;  
}
int main() {
    cout << "顺序char int double " << endl;
    print1();
    cout << "顺序char double int " << endl;
    print2(); // 1+(补7)+8+4 =20; 根据规则二不是 double(8) 的整数倍加4
    return 0;
}
代码结果:
顺序char int double 
16 1 4 8   # 规则一:1+(补3)+4+8=16
顺序char double int 
24 1 8 4   #分析 1+(补7)+8+4 =20; 根据规则二不是 double(8) 的整数倍,需20+4=24

图例

3.结构体成员为结构体对象时

#include <iostream> 
using namespace std;
struct X {
    char a;
    int b;
    double c;
};
struct Y {
    X b;
    char a;  
};
void print() {
    cout << "X 的内存大小为:" << sizeof(X) << endl;
    cout << "Y 的内存大小为:" << sizeof(Y) << endl;
}
int main () {
    print();
    return 0;
}
代码结果:
X 的内存大小为:16   # 1+(补3)+4+8=16
Y 的内存大小为:24   # b 前 a 后 :16+1+(补7,补至8的倍数) = 24 
# 若 a 前 b 后:1+(补7,补至8的倍数)+16=24

分析:

计算Y的存储长度时,在存放第二个元素b时的初始位置是在double型的长度8的整数倍处,而非16的整数倍处,即系统为b所分配的存储空间是第8~23个字节。

如果将Y的两个元素char型的a和X型的b调换定义顺序(b 前a 后),则系统为b分配的存储位置是第0~15个字节,为a分配的是第16个字节,加起来一共17个字节,不是最长基本类型double所占宽度8的整数倍,因此要补齐到8的整数倍,即24

修改默认对齐数

预处理指令: #pragma

#include <iostream>
using namespace std;
struct s1
{
  double a;
  char b;
  int c;
};
#pragma pack(4)  //数据成员对齐数 = min( 修改后的默认对齐数(4) ,该数据类型大小 )
struct s2
{
  char a;
  struct s1 s11;
  double b;
};
#pragma pack()
int main()
{
  printf("s1的大小:%d\n", sizeof(s1));
  printf("s2的大小:%d\n", sizeof(s2));
  return 0;
}
代码结果:
s1的大小:16
s2的大小:28   # 1+(补3,补至4的倍数)+16+8=28
  1. char a; // 地址从0开始,第一个字节存的是char
  2. struct s1 s11; // min( 4 ,max(double,char,int)) = 4 ,此时下标是1,1不是4的整数倍,补3个空字节,再存入s11(16个字节)
  3. double b; // min(4,8) = 4 ,此时下标是20,20是4的整数倍,再存入double
  4. 共存了1+3+16+8=28个字节,整体对齐数 = min(4,max( char, struct s1, double )) = 4,28是4的整数倍,不用补空字节。

int a[ ]中 a 与 &a区别

a 作为右值时与 &a[0] 意义相同,代表数组首元素地址

&a 是取数组 a 的首地址,&a+1是 &a + 5*sizeof(int);

a 是首元素首地址,即&a[0];&a 是数组首地址,虽然数值上相同,但是意义不相同。

#include <iostream>
using namespace std;
int main() {
    int a[5] = {1,2,3,4,5};
    int* ptr = (int* )(&a + 1);
    printf("%d,%d", *(a + 1), *(ptr - 1));  //2, 5
    return 0;
}

捋清楚这个代码的结果

#include <iostream>
using namespace std;
struct Test {
    int a;
    char* b;
    short c;
    char d[2];
    short e[4];
}*p;
int main() {
    cout << "sizrof(short) = " << sizeof(short) << endl;
    cout << "sizeof(struct Test) = " << sizeof(struct Test) << endl;
    cout << "sizeof(*p) = " << sizeof(*p) << endl;
    cout << "p = " << p << " p + 0x1 = " << p + 0x1 << endl;
    cout << "(unsigned long)p = " << p << " unsigned long)p + 0x1 = " << p + 0x1;
    return 0;
}

确定系统大小端

什么是大小端?

大端模式(Big.endian):字数据的高字节存储在低地址中,而字数据的低字节则存放在高地址中。

小端模式(Little_endian):字数据的高字节存储在高地址中,而字数据的低字节则存放在低地址中。

图例

确定系统大小端代码

#include <iostream> 
using namespace std;
int checkSystem() {
    union check{
        int i;
        char ch;     
    }c;
    c.i = 1;
    return (c.ch == 1);
}
int main () {
    int a = checkSystem();
    cout << a;   //输出为1 则为小端模式,0 则为大端模式
    return 0;
}

一道考验题

大端模式与小端模式下的输出结果分别是什么?

#include <iostream>
using namespace std;
void print() {
    union check{
      int i;
      char a[2];
    }*p, u;
    p = &u;
    p->a[0] = 0x39;
    p->a[1] = 0x38;
    cout << "p->i = " << p->i;
}
int main() {
    print();
    return 0;
}
小端模式下代码运行结果:
p->i = 14393

结果分析

3889(h) = 14393(d)


再来一道网络难题

计算一下输出结果!

#include <iostream>
using namespace std;
int main() {
    int a[4] = {1,2,3,4};
    int* ptr1 = (int* )(&a + 1);
    int* ptr2 = (int* )((long)a + 1);
    printf("%x, %x", ptr1[-1], *ptr2);
    return 0;
}
代码运行结果:
4, 2000000   # 在小端模式下
4, 100       # 在大端模式下

分析

ptr1:&a+1的值强制转换成int*类型,赋值给int*类型的变量 ptr,ptrl 肯定指到数组a的下一个int类型数据了。ptr1[-1]被解析成* (ptr1-1),即ptrl往后退4字节,所以其值为0x4。

ptr2:按照上面的讲解,(int)a+1的值是元素a[0]的第2个字节的地址,然后把这个地址强制转换成int*类型的值赋给ptr2,也就是说*ptr2的值应该为元素a[0]的第2个字节开始的连续4字节的内容。

小端模式下

大端模式下

参考文章链接

  1. Byte and Bit Order Dissection
  2. 大小端对字节序和位序的影响

二维数组

内存相当于尺子,最小单位为字节;二维数组也是存在尺子当中的。

看成这样很重要

来一道题

#include <iostream>
using namespace std;
int main() {
    int a[3][2] = {(0,1), (2,3), (4,5)};
    int* p = a[0];
    /*for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 2; j ++) 
            cout << a[i][j] << " ";
        cout << endl;
    }*/
    printf("%d", p[0]);  //1
    return 0;
}

分析

花括号里面嵌套的是小括号,而不是花括号!这里是花括号里面嵌套了逗号表达式,
其实这个赋值就相当于int a[3][2]={1, 3, 5}。

再来一道号称很难做对的题 &p[4][2] - &a[4][2]

#include <stdio.h>
int main() {
    int a[5][5];
    int (*p)[4];
    p = a;
    printf("a_ptr = %#p, p_ptr = %#p\n", &a[4][2], &p[4][2]);
    printf("%p, %d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
    return 0;
}
代码运行结果:
a_ptr = 0x7fff23bcfdc8, p_ptr = 0x7fff23bcfdb8
0xfffffffffffffffc, -4

分析理解

  • 首先明确数组名 a 作为右值时,代表数组首元素地址,即 a 代表 a[0]地址。
  • &a[4][2]表示的是&a[0][0]+4*5*sizeof(int)+ 2 * sizeof(int)
  • p 是指向四个元素的数组指针,也就是说 p+1 表示指针 p 向后移动“包含4个int类型元素的数组”。
  • 由于p被初始化为&a[0],所以&p[4][2]表示的是&a[0][0]+4 *4sizeof(int)+2*sizeof(int)


二维数组与二维指针

二维数组参数与二维指针参数关系


函数指针

简单看下函数指针用法

#include <stdio.h>
#include <string.h>
char* fun(char* p1, char* p2) {
    int i = 0;
    i = strcmp(p1, p2);
    if (i == 0) return p1;
    else return p2;
}
int main() {
    char * (*pf)(char* p1, char* p2);
    pf = &fun;
    (*pf)("aa", "bb");
    return 0;
}

理解* (int* )&p = (int)Function做了什么

#include <stdio.h>
void Function() {
    printf("call Function");
}
int main() {
    void (*p)();
    * (int* )&p = (int)Function;
    (*p)();
    return 0;
}

分析

  • (int*)&p表示将地址强制转换成指向int类型数据的指针。
  • (int)Function表示将函数的人口地址强制转换成int类型的数据。
  • “* (int* ) &p= (int) Function;"表示将函数的人口地址赋值给指针变量p。

再来一个(*(void(*)())0)()的理解分析

  • 第1步:void( * ) (),可以明白这是-一个函数指针类型。这个函数没有参数,没有返回值;
  • 第2步:(void( * ) ())0,这是将0强制转换为函数指针类型,0是一个地址,也就是说一个函数保存在首地址为0的一段区域内
  • 第3步:(*(void(*)())0),这是取0地址开始的一段内存里面的内容,其内容就是保存在首地址为0的一段区域内的函数;
  • - 第4步:( * (void( * ) ())0)(),这是函数调用。

函数指针数组与函数指针数组指针

函数指针数组

  • char* (*pf)(char* p)不难理解!
  • char* (*pf[3])(char* p) 就是函数指针数组。
  • pf并非指针,而是一个数组名,数组存储了3个指向函数的指针

函数指针数组指针

  • char* (*(*pf)[3])(char* p)就是函数指针数组指针!!!
    pf 是实实在在的指针,这个指针指向一个包含了3个元素的数组;这个数组里面存的是指向函数的指针
//指针函数数组指针的应用
#include <stdio.h>
#include <string.h>
char* func1(char* p) {
    printf("%s\n", p);
    return p;
}
char* func2(char* p) {
    printf("%s\n", p);
    return p;
}
char* func3(char* p) {
    printf("%s\n", p);
    return p;
}
int main() {
    char* (*a[3])(char* p);
    char* (*(*pf)[3])(char* p);
    pf = &a;
    a[0] = &func1;
    a[1] = &func2;
    a[2] = &func3;
    pf[0][0]("func1");  //等价(*pf)[0]("func1")
    pf[0][1]("func2");
    pf[0][2]("func3");
    return 0;
}

typedef

与回调函数相关的应用:C/C++ typedef 用法

STL标准库

List底层实现原理

原理图

deque 底层原理

priority_queue 底层原理(容器适配器)




相关文章
|
2月前
|
存储 算法 C++
C++ STL 初探:打开标准模板库的大门
C++ STL 初探:打开标准模板库的大门
112 10
|
1天前
|
算法 C语言 C++
【c++丨STL】list的使用
本文介绍了STL容器`list`的使用方法及其主要功能。`list`是一种双向链表结构,适用于频繁的插入和删除操作。文章详细讲解了`list`的构造函数、析构函数、赋值重载、迭代器、容量接口、元素访问接口、增删查改操作以及一些特有的操作接口如`splice`、`remove_if`、`unique`、`merge`、`sort`和`reverse`。通过示例代码,读者可以更好地理解如何使用这些接口。最后,作者总结了`list`的特点和适用场景,并预告了后续关于`list`模拟实现的文章。
19 7
|
19天前
|
存储 编译器 C语言
【c++丨STL】vector的使用
本文介绍了C++ STL中的`vector`容器,包括其基本概念、主要接口及其使用方法。`vector`是一种动态数组,能够根据需要自动调整大小,提供了丰富的操作接口,如增删查改等。文章详细解释了`vector`的构造函数、赋值运算符、容量接口、迭代器接口、元素访问接口以及一些常用的增删操作函数。最后,还展示了如何使用`vector`创建字符串数组,体现了`vector`在实际编程中的灵活性和实用性。
45 4
|
20天前
|
C语言 C++ 容器
【c++丨STL】string模拟实现(附源码)
本文详细介绍了如何模拟实现C++ STL中的`string`类,包括其构造函数、拷贝构造、赋值重载、析构函数等基本功能,以及字符串的插入、删除、查找、比较等操作。文章还展示了如何实现输入输出流操作符,使自定义的`string`类能够方便地与`cin`和`cout`配合使用。通过这些实现,读者不仅能加深对`string`类的理解,还能提升对C++编程技巧的掌握。
45 5
|
20天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
30 2
|
25天前
|
存储 算法 Linux
【c++】STL简介
本文介绍了C++标准模板库(STL)的基本概念、组成部分及学习方法,强调了STL在提高编程效率和代码复用性方面的重要性。文章详细解析了STL的六大组件:容器、算法、迭代器、仿函数、配接器和空间配置器,并提出了学习STL的三个层次,旨在帮助读者深入理解和掌握STL。
42 0
|
4天前
|
存储 编译器 C语言
【c++丨STL】vector模拟实现
本文深入探讨了 `vector` 的底层实现原理,并尝试模拟实现其结构及常用接口。首先介绍了 `vector` 的底层是动态顺序表,使用三个迭代器(指针)来维护数组,分别为 `start`、`finish` 和 `end_of_storage`。接着详细讲解了如何实现 `vector` 的各种构造函数、析构函数、容量接口、迭代器接口、插入和删除操作等。最后提供了完整的模拟实现代码,帮助读者更好地理解和掌握 `vector` 的实现细节。
16 0
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
79 4
|
2月前
|
存储 安全 编译器
在 C++中,引用和指针的区别
在C++中,引用和指针都是用于间接访问对象的工具,但它们有显著区别。引用是对象的别名,必须在定义时初始化且不可重新绑定;指针是一个变量,可以指向不同对象,也可为空。引用更安全,指针更灵活。
|
2月前
|
存储 程序员 C++
C++常用基础知识—STL库(2)
C++常用基础知识—STL库(2)
74 5