C语言基础之函数、虚拟键盘的使用、数组、指针、动态申请内存、内存泄漏

如果在定义函数时没有注明返回类型,则默认为int(不推荐使用)。

函数调用有两种类型:

1.先定义再调用:在调用函数之前必须已经被定义

2.函数声明+函数调用:要调用在后面定义的函数必须在调用函数之前提前声明该函数的原型,否则无法识别函数。原型可不写出形参名。

函数声明+函数调用的例子1:

#include<stdio.h>

int add(int, int);

void main() {
	int num1 = 1;
	int num2 = 2;
	printf("%d + %d is : %d\n", num1, num2, add(num1, num2));
	system("pause");
}

int add(int num1, int num2) {
	return num1 + num2;
}
输出结果:

1 + 2 is : 3

函数声明+函数调用的例子2,跨文件调用:

头文件中定义hehe.h文件:

#include<stdio.h>

void run();

源文件中定义hehe.c文件:

#include"hehe.h"

void run() {
	printf("run run run\n");
	system("pause");
}

demo.c中调用run方法:

#include"hehe.h"

void main() {
	run();
}
输出结果:

run run run

虚拟键盘的使用例子:

#include<Windows.h>

void main() {
	// 单个按键Win
	// 虚拟键盘事件,第一个参数是键的地址,第三个参数0表示按下,2表示松开
	keybd_event(0x5b, 0, 0, 0);
	// 键的地址可以通过Virtual-Key Codes查询到
	keybd_event(0x5b, 0, 2, 0);

	// 组合键Win + E
	keybd_event(0x5b, 0, 0, 0);
	keybd_event('E', 0, 0, 0);
	keybd_event('E', 0, 2, 0);
	keybd_event(0x5b, 0, 2, 0);
}


数组是可以在内存中连续存储多个元素的结构。

一维数组地址的引用:&数组名[下标表达式] 或 数组名+/-整数

如&a[0]、&a[2 * i - 1]、a、a + 2 * i - 1

C语言中用数组名表示数组的首地址,即第一个元素的地址。地址加减整数就是求当前元素后面或前面第几个元素的地址。字符串其实就是一个字符数组,故字符串名也是其首地址。

*(地址):取地址里面保存的值。

数组名就是首地址、数组在内存中是连续存储的例子:

#include<stdio.h>

void main() {
	int num[4] = { 6,9,9,1 };
	printf("num首地址是:%x\n", num);
	for (int i = 0; i < 4; i++) {
		printf("num[%d]是%d = %d,其地址是%x = %x\n", i, num[i],*(num + i), &num[i], num + i);
	}
	system("pause");
}
输出结果:

num首地址是:41fc9c
num[0]是6 = 6,其地址是41fc9c = 41fc9c
num[1]是9 = 9,其地址是41fca0 = 41fca0
num[2]是9 = 9,其地址是41fca4 = 41fca4
num[3]是1 = 1,其地址是41fca8 = 41fca8

由上面例子知:

num[i]与*(num + i)等价

&num[i]与num + i等价

定义数组时,数组的长度不能是const定义的常量,可以是define定义的常量。因为const定义的常量可以通过变量名获取到内存地址进行修改,而define只是在编译时替换。

数组名就是首地址即常量,故数组之间不能进行赋值运算:

int a[2] = {1,2};
int b[2] = {3,4};
// a = b; // 类似于 0 = 1; 语句,常量之间是不能赋值的。

数组除了字符串以外,不能整体输入输出,只能逐个输入输出。

数组作为参数的时候传递的是首地址:

#include<stdio.h>

void hehe(int a[10]) {
	printf("%d\n", sizeof(a));
	// 数组不作为参数进行传递时,就是实际的大小
	int b[20] = { 0 };
	printf("%d\n", sizeof(b));
}
void main() {
	int num[10] = { 0 };
	hehe(num);
	system("pause");
}
输出结果:

4
80

二维数组a[M][N]中的a[i]是第i行的首地址,故&a[i][j]等价于a[i] + j。

a[i][j] 等价于 *(&a[i][j])又等价于*(a[i] + j)。

数组初始化形式:

// 赋值为0,二维数组全部为0
int a[1][2] = { 0 };
// 二维数组可以当做一个一维数组,每一个元素又是一个一维数组
int a[2][3] = {{1,2,3},{2,3,4}};
// 大括号初始化的话,行数可以省略
int a[][3] = {{1,2,3}};
// 列坐标不能省略
// int a[3][] = {{0},{1},{2}};// 错误的初始化

当声明语句中提供有全部元素的初始值时,第一维的大小可以省略。N维数组用大括号初始化时,只有第一维的大小可以省略。

维数决定了数组中元素的组织方式以及访问元素所用的下标个数,但本质上讲所有的数组在内存中都是一维线性的,所有元素都是连续排列的,中间没有间隔。以二维数组为例,内存中是先放第1行的元素,再放第2行的元素,以此类推。

多维数组不同下标的关系类似于数字的不同位数,最左边的下标变换最慢,最右边的下标变换最快。

指针变量也是变量,占据一定的内存空间,有地址,因此可以用一个指针指向它,这称为指向指针的指针或二级指针。所有指针在32位系统下都是四个字节即32位。

如:

#include<stdio.h>

void main() {
	int a = 1;
	int b = 2;
	int *p = &a;
	int **pp = &p;
	**pp = 8; // 相当于*(*pp),即*号的运算顺序是从右向左
	*pp = &b;
	**pp = 3;
	printf("a = %d, b = %d\n", a, b);
	system("pause");
}
输出结果:

a = 8, b = 3

改变一个变量的值需要得到该变量的地址。如果变量是数据则需要使用指针,如果变量是指针,则需要二级指针来保存指针变量的地址。原则上说,指针类型和指针所指向的类型应当是相同的。

改变指针p所指向的地址例子:

#include<stdio.h>

char a = 'A';
char b = 'B';
char c = 'C';

void change(char *p) {
	p = &c;
	printf("%c\n", *p);
}

void change1(char **p) {
	*p = &c;
}
void main() {
	char *p = &a;
	printf("指针p指向的值为%c\n", *p);
	p = &b;
	printf("指针p指向的值为%c\n", *p);
	change(p); // 值传递
	printf("指针p指向的值为%c\n", *p);
	change1(&p); // 地址传递
	printf("指针p指向的值为%c\n", *p);
	system("pause");
}
输出结果:

指针p指向的值为A
指针p指向的值为B
C
指针p指向的值为B
指针p指向的值为C

直接访问:按变量地址存取变量值。

间接访问:通过存放变量地址的变量去访问变量。

指针和整数的加减返回结果可能也是一个指针即地址值,问题的关键在于这个指针到底指向什么地方。通俗地说,“指针+整数”用于将指针向后移动“sizeof(指针类型) * 整数”个内存单元,而“指针 - 整数”用于将指针向前移动“sizeof(指针类型) * 整数”个内存单元。由于编译器不会检查这种移动的有效性,故指针和整数的加减适宜在数组或者动态申请的内存内进行
例子:

#include<stdio.h>

void main() {
	int a[5] = { 1,3,1,6,9 };
	int *p = a;
	for (int i = 0; i < 5; i++) {
		printf("a[%d]=%d=%d,其地址是:%x=%x\n", i, *(p + i), p[i], p + i, &p[i]);
	}
	system("pause");
}
输出结果:

a[0]=1=1,其地址是:3efdc8=3efdc8
a[1]=3=3,其地址是:3efdcc=3efdcc
a[2]=1=1,其地址是:3efdd0=3efdd0
a[3]=6=6,其地址是:3efdd4=3efdd4
a[4]=9=9,其地址是:3efdd8=3efdd8
即:*(p + i) 等价于 p[i];(p + i) 等价于 &p[i]。

指针的大小比较:

对两个毫无关联的指针比较大小是没有意义的,因为指针只代表了“位置”这一信息,但是如果两个指针所指向的元素位于同一个数组(或同一块动态申请的内存中),指针的大小比较反映了元素在数组中的先后关系。

指针相减:

两个同类型的指针相减返回值是一个有符号的整数

(指针1的值 - 指针2的值) / 指针类型占用的内存字节数

举例来说,指针p1指向a[i],指针p2指向a[j],那么p1 - p2 = i - j。两个指针的距离并不是其值简单做差,还要除以“指针类型占用的内存字节数”。指针相减多用于同一块内存(如数组或一块动态申请的内存)中,如果两个指针所指向的元素没有结构上的关系,指针相减的结果将不可预测。

指针与数组
数组名是表示数组首地址的地址常量。

注意:数组名a不代表整个数组,只代表数组首元素的地址,“p = a;”的作用是“把a数组的首地址赋值给指针变量p”,而不是“把数组a各元素的值赋给p”。

指针引用多维数组

行指针每加1,走一行:

a代表第0行首地址

a + 1代表第1行首地址

a + 2代表第2行首地址

即:a + i代表行号为i的行首地址(按行变化)

列指针每加1,走一列:

a[0]代表a[0][0]的地址

a[0] + 1代表a[0][1]的地址

a[0] + 2代表a[0][2]的地址

即:a[i] + j代表a[i][j]的地址

如:

#include<stdio.h>

void main() {
	int a[2][3] = { {1,3,1},{6,9,9} };
	for (int i = 0; i < 2; i++) {
		for (int j = 0; j < 3; j++) {
			printf("%3d = %-3d", *(a[i] + j), *(*(a + i) + j));
		}
		printf("\n");
	}
	
	system("pause");
}
输出结果:

  1 = 1    3 = 3    1 = 1
  6 = 6    9 = 9    9 = 9

函数指针

如果在程序中定义了一个函数,在编译时编译系统为函数代码分配一段存储空间,这段存储空间的起始地址称为该函数的指针。可以定义一个指向函数的指针变量用来存放某一函数的起始地址,这就意味着此指针变量指向该函数。如:int (*p) (int, int);

定义p是指向函数的指针变量,它可以指向类型为整型且有两个整型参数的函数。p的类型用int (*)(int, int)表示。

如:

#include<stdio.h>

int max(int a, int b) {
	if (a > b) {
		return a;
	}
	else {
		return b;
	}
}
void main() {
	int(*p)(int, int);
	p = max;
	printf("%d = %d\n", p(5, 2), max(5,2));
	system("pause");
}
输出结果:

5 = 5

简单地把整数赋给指针是不允许的,下列代码是错误的:

int *p = 0x12345678;

如果实在有必要对某个内存地址进行访问,可以通过强制类型转换来完成,如:

int *p = (int *)0x12345678;

void指针与空指针:

void *指针是一种特殊的指针,不指向任何类型的数据,故不知道其结束地址,所以不能读出其内容,如果需要用此地址指向某类型的数据,应先对地址进行类型转换。可以在程序中进行显式的类型转换,也可以由编译系统自动进行隐式转换。

指针变量可以有空值,即该指针变量不指向任何变量,可以这样表示:p  = NULL;

例子:

#include<stdio.h>

void main() {
	int a = 1;
	void *p = &a;
	// printf("%d\n", *p); // 非法的间接寻址
	printf("%d\n", *((int*)p));
	system("pause");
}
输出结果:

1

变量的地址与进程的地址关系:

进程的首地址是变动的,变量相对进程首地址是固定的。

堆区:程序员来管理的堆,内存自己分配。

栈区:系统自动分配自动管理的就是栈,数组就在栈区。

主动申请的都在堆区,其他的都在栈区。

malloc和free是C标准库中提供的两个函数,用以动态申请和释放内存,malloc()函数的基本调用格式是:

void *malloc(unsiged int size);

参数size是个无符号整型数,用户由此控制申请内存的大小,执行成功时,系统会为程序开辟一块大小为size个内存字节的区域,并将该区域的首地址返回,用户可利用该地址管理并使用该块内存。如果申请失败(比如内存不够用)返回空指针NULL。malloc()函数返回类型是void *,用其返回值对其他类型指针赋值时,必须进行显式转换。size仅仅是申请字节的大小并不管申请的内存块中存储的数据类型,因此,申请内存的长度须由程序员通过“长度 * sizeof(类型)”的方式给出,如申请一片内存,内存有一个首地址,传递给一个指针:

int *p = (int *)malloc(10 * sizeof(int));

free就是释放内存,如:free(p);


除了malloc与free外,C语言标准库函数还提供了calloc函数用以动态申请内存,和malloc函数以字节为单位申请内存不同,calloc函数是以目标对象为单位分配的,目标对象可以是数组,也可以是结构体等。

calloc函数的原型为:

void *calloc(size_t num, size_t size);

calloc()函数返回类型也是void *,需要强制转换才能为其他类型的指针赋值。calloc需要两个参数来指定申请内存块的大小:对象的个数sum和单个对象占据的内存字节数size。


为已经分配的内存重新分配空间并复制内存:realloc()函数

void *realloc(void *ptr, size_t size);

realloc()函数也需要两个参数:已分配的内存地址和重新分配的字节数。


内存泄漏:

释放动态内存并不意味着指针会消亡,也不意味着指针的值会改变。如:

#include<stdio.h>

void main() {
	int *p1 = (int *)malloc(10 * sizeof(int));
	// 判断内存是否申请成功
	if (p1 == NULL) {
		printf("内存申请失败,退出");
		return;
	}
	// 无符号的十六进制,并以小写abcdef表示
	printf("p的值是%x\n", p1);
	// 无符号的十六进制,并以大写ABCDEF表示
	printf("p的值是%X\n", p1);
	int *q = p1;
	free(p1);
	// free(q); // 释放已经释放了的内存会崩溃
	// 使用已经释放的内存是非法的。
	printf("%d\n", *p1);
	printf("p的值是%x\n", p1);
	// %p是指向变量的地址,这里就是p的地址
	printf("p的值是%p\n", p1);
	system("pause");
}
输出结果:

p的值是21c3c8
p的值是21C3C8
-572662307
p的值是21c3c8
p的值是0021C3C8

指针消亡,动态内存是否会自动释放呢?否

如果没有释放内存,但记录该块内存的指针消亡了或者是指针的值发生了改变,这块内存将永远得不到回收,造成了内存泄漏,如果程序长时间运行的话,不断的泄漏可能使得系统内存耗尽而崩溃。

malloc处理变动需求的例子:

#include<stdio.h>

void main() {
	printf("请输入要生成随机数的个数:");
	int num;
	scanf_s("%d", &num);
	int *p = (int *)malloc(num * sizeof(int));
	for (int i = 0; i < num; i++) {
		p[i] = rand() % 100;
		printf("%d\n", p[i]);
	}
	free(p);
	system("pause");
}
输出结果:

请输入要生成随机数的个数:4
41
67
34
0

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值