DSLBQ'S_BLOG

C: 指针与内存

标准函数库[链接]

小端序与大端序

大端序符合人类阅读习惯,但多数计算机平台采用小端序,这跟芯片的制作工艺有关,例如0x5ecff7f0小端序在内存中显示为 f0 f7 cf 5e

指针

指针操作

一个int类型指针 + 1相当于加4个字节,因为int类型分配的空间就是4个字节(与平台架构有关),外挂程序就是通过修改指针赋值实现某些游戏功能。

指针转换

指针可以进行强制转换,但转换后获取的值可能会有偏差,例如char类型的指针与int类型的指针,虽然&ab指向的是同一个地址,但char取值只取一个字节,而int取值为四个字节:

char a = 'd'; 
int* b = (int*) &a;
printf("a = 0x%x\n", *&a); // *&a = 0x64 
printf("b = 0x%x\n", *b); // *b = 0xcccccc64

out:
a = 0x64
b = 0xcccccc64

二级指针/三级指针

二级指针存放一级指针的地址,三级指针存放二级指针地址

char a = 'd';
char* b = &a;
char** c = &b;
printf("a = 0x%x\n", *&a);
printf("b = 0x%x\n", *b);
printf("c = 0x%x\n", **c);
printf("c = 0x%p\n", *c);

out:
a = 0x64
b = 0x64
c = 0x64
c = 0x14fcd4

指针外挂

可以使用Cheat Engine对相关地址的值进行修改,也可以自己写个C程序对指针进行修改,低权限进程无法修改高权限进程值。

指针数组

数组变量默认就是一个指针,如下所示:a是一个指针,所以*pa == apa[0] == a[0], pa[1] == a[1]b是一个值,所以*pb == &b。指针也可以指向具体某个数组值的地址:*pa4 = &a[4]

int a[5] = {1,2,3,4,5};
int b = 6;
int* pa = a;
int* pb = &b;
int* pa4 = &a[4]; // 指针也可以指向具体某个数组值的地址

数组、函数、指针

数组传递参数,由于数据变量是指针类型,所以默认传进函数的是指针,也就意味着在主函数内部修改值,外部的值也是会跟着改变

void test(int a[]) {
    for (int i = 0; i < 3; i++) {        
        printf("i = %d\n", a[i]);
        a[i] = a[i] + 100;
    }
}

void test2(int c) {
    printf("c = %d\n", c);
    c = 100;
}

int main() {
    int b[] = {1,2,3};
    test(b);
    for (int j = 0; j < 3; j++) {
        printf("j = %d\n", b[j]);
    }
    /* 打印被修改的值,而不是1,2,3: 
    j = 101
    j = 102
    j = 103 */

    int d = 2;
    test2(d);
    printf("d = %d\n", d);
    /* 打印原始值,不会因为函数值被修改而改变
    d = 2*/
}

函数名默认也是一个指针,所以数组值也可以是一组函数,需要注意返回类型必须一样

void* f[2] = { test, test2 };
printf("f[0] = %p\n", f[0]);
printf("f[1] = %p\n", f[1]);

out:
f[0] = 00007FF70C0613D9
f[1] = 00007FF70C0613E3

定义一个新函数指向函数指针(会有很多这种使用场景,根据具体业务使用)。

int b[] = { 1,2,3 };
void(*pf)(int[]) = test;
pf(b);// 调用新函数

pf的值是一个二级指针,指向一个新的复制函数的地址。

方法里面调用函数指针数组:

void test() {
    printf("test\n");
}

void test3() {
    printf("test3\n");
}

void test2(void (*c[3])()) {
    c[2]();
    return;
}

int main() {  
    void(*ptest)() = test;
    void* c[3] = {ptest, test, test3}; // 一般指针指向函数,在方法里面调用需要强转
    void(*b[3])() = {ptest, test, test3}; // 声明为函数指针数组
    test2(b);
}

自定义类型

在上面的方法用,void* c[3] = {ptest, test, test3}只是一般指针指向函数,而不是函数指针,如果传入的参数只是一般的指针,而在方法里面调用,此时就需要自定义类型对函数进行强转

typedef void (*PT)(); // 定义函数类型

void test() {
    printf("test\n");
}

void test3() {
    printf("test3\n");
}

void test2(void* c[3]) {
    PT p = (PT)c[0];// 将一般指针强转为函数指针
    p();
    return;
}


int main() {
    void(*ptest)() = test;
    void* c[3] = { ptest, test, test3 }; // 一般指针指向函数
    void(*b[3])() = { ptest, test, test3 }; // 声明为函数指针数组
    test2(c);
}

Const 声明位置

	int a = 6;
	int b = 7;
	int* const pa = &a;
	int const* pb = pa;
	pa = &b; // 报错,变量已声明为常量,只能指向&a,不能修改为其他指向
	*pa = 10;
	pb = &b; // 报错,变量指针已声明为常量,不能修改指针里面的值
	*pb = 10;
	const int* const pc = &a; // 全部只读

结构体:根据声明类型决定结构体内存大小,取地址为&s.name,&s.age,结构体大小因为涉及到内存里的对齐(例如int4个字节,char1个字节,结构体为8个字节),所以会比实际的声明大一些,获取结构体大小直接使用sizeof就可以了。

#include <iostream>
struct Student {
	char* name;
	char age;
	int clazz;
};

typedef struct dslbq {
	char a : 2;
	int b;
	short c;
} d; // 定义结构体为新类型

struct dslbq {
	char a : 2;
	int b;
	short c;
} d; // 声明d变量
、

int main() {	
	Student s;
	s.age = 30;
	s.name = (char*)"dslbq";
	s.clazz = 3;

	Student* ps = &s; // 取结构体地址
	ps -> name = (char*)"beijing"; // 写结构体声明地址
        printf("%s\n", ps -> name); // 读结构体声明地址
}

数组变量初始化时,默认分配一块空间,并且这个变量为const,不能重新指向另一个地址,因为已经分配,避免浪费。

char b[3];
b[0] = 'aa';
b = { 'aa' }; // 报错

Union:只能使用其中的一个字段,赋值时会把其他值同时更新

union Human {
	char a;
	int age;
};

struct Student {
	char* name;
	char age;
	int clazz;
	union { // 匿名联合体
		int ua;
		int ub;
	};
};

int main() {
	Human human;
	human.a = 'b';
	human.age = 21;
}

a最初赋值为98
a、age全部更新为21

枚举:对设定值后面的值依次累加1

enum Enum {
	enum1, // 设置值:0
	enum2, // 设置值:1
	enum3  // 设置值:2
};

int main() {
	Enum e;
	e = enum1;
	e = enum2;
}

文件包含

函数声明写在.h文件,函数实现写在.c文件,.c文件include .h实现具体内容,业务调用直接包含.h文件,这样就可以只暴露接口,隐藏具体实现的代码

引入三方库

引入.h.c文件到工程指定目录

宏定义(预处理):Windows大量的使用宏定义,是因为声明变量、函数等需要在栈中开辟空间,而用宏就直接进行文本替换,不需要进栈出栈走函数的那种体系,效率更高。

#define _CRT_SECURE_NO_WARNINGS
#define PX printf("main\n");
#define PP(x) printf("main=>%s\n", x);
// 格式:预处理指令 宏[宏参数] 替换体
#ifdef windows // 条件预编译,比如不同操作系统采用不同操作
int x = 1;
#else
int x = 0;
#endif

可变参数

#include <stdarg.h>
#define PR(...) printf(__VA_ARGS__) // 可变宏定义参数
#define VAR(x) V ## x // 动态语法,例如VAR(2)则动态声明V2这个变量,##:将后面的x粘到前面的参数
int pr(int num, ...) { // 可变函数参数
	va_list valist;
	va_start(valist, num);

	for (int i = 0; i < num; i++){
		int arg = va_arg(valist, int);
		printf("%d\n", arg);
	}
	va_end(valist);
	return 0;
}
int main(int argc, char* argv[]) {
	int VAR(1) = 2;
	pr(5, 2, 3, 4, 5, 6);
        PR("%d\n", 12);
}

内存

栈区 / 堆栈(stack)

先进后出,程序运行时由程序自动分配,存放函数的参数值、局部变量的值,程序结束时由程序自动释放。

堆区(heap)

先进先出,程序运行时在内存开辟另一块存储区域,一般由程序员分配释放,用malloccallocrealloc等分配内存的函数分配得到的就是在堆上。

全局区(静态区static)

全局变量、函数体等

编译过程

  1. 预处理(Preprocessiong)
    预处理指令文本替换:#include,宏定义
  2. 编译成汇编(Compilation)
    转换成对应汇编代码
  3. 汇编转二进制(Assemble)
    汇编代码转换成二进制代码
  4. 链接(Linking)
    链接操作系统库代码,打包到本程序中

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注