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 底层原理(容器适配器)




相关文章
|
3天前
|
存储 算法 C++
C++提高篇:泛型编程和STL技术详解,探讨C++更深层的使用
文章详细探讨了C++中的泛型编程与STL技术,重点讲解了如何使用模板来创建通用的函数和类,以及模板在提高代码复用性和灵活性方面的作用。
12 2
C++提高篇:泛型编程和STL技术详解,探讨C++更深层的使用
|
3天前
|
编译器 C++
【C++核心】指针和引用案例详解
这篇文章详细讲解了C++中指针和引用的概念、使用场景和操作技巧,包括指针的定义、指针与数组、指针与函数的关系,以及引用的基本使用、注意事项和作为函数参数和返回值的用法。
11 3
|
1天前
|
存储 Go
Go: struct 结构体类型和指针【学习笔记记录】
本文是Go语言中struct结构体类型和指针的学习笔记,包括结构体的定义、成员访问、使用匿名字段,以及指针变量的声明使用、指针数组定义使用和函数传参修改值的方法。
|
24天前
|
C++
C++(十八)Smart Pointer 智能指针简介
智能指针是C++中用于管理动态分配内存的一种机制,通过自动释放不再使用的内存来防止内存泄漏。`auto_ptr`是早期的一种实现,但已被`shared_ptr`和`weak_ptr`取代。这些智能指针基于RAII(Resource Acquisition Is Initialization)原则,即资源获取即初始化。RAII确保对象在其生命周期结束时自动释放资源。通过重载`*`和`-&gt;`运算符,可以方便地访问和操作智能指针所指向的对象。
|
24天前
|
C++
C++(九)this指针
`this`指针是系统在创建对象时默认生成的,用于指向当前对象,便于使用。其特性包括:指向当前对象,适用于所有成员函数但不适用于初始化列表;作为隐含参数传递,不影响对象大小;类型为`ClassName* const`,指向不可变。`this`的作用在于避免参数与成员变量重名,并支持多重串联调用。例如,在`Stu`类中,通过`this-&gt;name`和`this-&gt;age`明确区分局部变量与成员变量,同时支持链式调用如`s.growUp().growUp()`。
|
1月前
|
存储 算法 编译器
[C++] STL简介
[C++] STL简介
23 1
|
1月前
|
存储 安全 C++
C++:指针引用普通变量适用场景
指针和引用都是C++提供的强大工具,它们在不同的场景下发挥着不可或缺的作用。了解两者的特点及适用场景,可以帮助开发者编写出更加高效、可读性更强的代码。在实际开发中,合理选择使用指针或引用是提高编程技巧的关键。
24 1
|
1月前
|
存储 算法 C++
C++ STL应用宝典:高效处理数据的艺术与实战技巧大揭秘!
【8月更文挑战第22天】C++ STL(标准模板库)是一组高效的数据结构与算法集合,极大提升编程效率与代码可读性。它包括容器、迭代器、算法等组件。例如,统计文本中单词频率可用`std::map`和`std::ifstream`实现;对数据排序及找极值则可通过`std::vector`结合`std::sort`、`std::min/max_element`完成;而快速查找字符串则适合使用`std::set`配合其内置的`find`方法。这些示例展示了STL的强大功能,有助于编写简洁高效的代码。
34 2
|
1月前
|
安全 NoSQL Redis
C++新特性-智能指针
C++新特性-智能指针
|
1月前
|
编译器 C++
virtual类的使用方法问题之在C++中获取对象的vptr(虚拟表指针)如何解决
virtual类的使用方法问题之在C++中获取对象的vptr(虚拟表指针)如何解决