C语言学习之路(高级篇)—— 变量和内存分布(上)

说明:该篇博客是博主一字一码编写的,实属不易,请尊重原创,谢谢大家!

数据类型

1) 数据类型概念

什么是数据类型?为什么需要数据类型?

数据类型是为了更好进行内存的管理,让编译器能确定分配多少内存。

我们现实生活中,狗是狗,鸟是鸟等等,每一种事物都有自己的类型,那么程序中使用数据类型也是来源于生活。

当我们给狗分配内存的时候,也就相当于给狗建造狗窝,给鸟分配内存的时候,也就是给鸟建造一个鸟窝,我们可以给他们各自建造一个别墅,但是会造成内存的浪费,不能很好的利用内存空间。

我们在想,如果给鸟分配内存,只需要鸟窝大小的空间就够了,如果给狗分配内存,那么也只需要狗窝大小的内存,而不是给鸟和狗都分配一座别墅,造成内存的浪费。

当我们定义一个变量,a = 10,编译器如何分配内存?计算机只是一个机器,它怎么知道用多少内存可以放得下10
所以说,数据类型非常重要,它可以告诉编译器分配多少内存可以放得下我们的数据。

狗窝里面是狗,鸟窝里面是鸟,如果没有数据类型,你怎么知道冰箱里放得是一头大象!

数据类型基本概念:

  • 类型是对数据的抽象;
  • 类型相同的数据具有相同的表示形式、存储格式以及相关操作;
  • 程序中所有的数据都必定属于某种数据类型;
  • 数据类型可以理解为创建变量的模具: 固定大小内存的别名;

在这里插入图片描述

2) 数据类型别名

示例代码:

#define _CRT_SECURE_NO_WARNINGS // VS不建议使用传统库函数,如果不用这个宏,会出现一个错,编号:C4996
#include <stdio.h> // std 标准 i input  输入   o  output 输出 
#include <stdlib.h> // strcpy   strcmp  strcat  strstr
#include <string.h> // // malloc  free

//程序入口
int main()
{
    
    

	system("pause"); // 按任意键暂停  阻塞功能
	return EXIT_SUCCESS; //返回 正常退出值  0
}

2.1 简化结构体关键字

typedef使用,简化结构体关键字 struct

struct Person
{
    
    
	char name[64];
	int age;
};
typedef struct Person  myPerson;

void test01()
{
    
    	
	// 未使用typedef,在初始化成员时需要加struct修饰
	struct Person p1 = {
    
     "张三", 19 };
	// 使用typedef,可以简化结构体关键字struct
	myPerson p2 = {
    
     "李四", 20 };
}

简化以上写法

//主要用途  给类型起别名
//语法  typedef  原名  别名
typedef struct Person
{
    
    
	char name[64];
	int age;
}myPerson;

void test01()
{
    
    	
	// 未使用typedef,在初始化成员时需要加struct修饰
	struct Person p1 = {
    
     "张三", 19 };
	// 使用typedef,可以简化结构体关键字struct
	myPerson p2 = {
    
     "李四", 20 };
}

2.2 区分数据类型

typedef使用,区分数据类型

// 2、区分数据类型
void test02()
{
    
    
	char* p1, p2; // p1是char *  而 p2 是char
}

通过c++里面的typeid方法去验证p1p2的数据类型

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;

int main()
{
    
    
	char* p1, p2;
	printf("p1的数据类型为:%s\n", typeid(p1).name());
	printf("p2的数据类型为:%s\n", typeid(p2).name());

	system("pause");
	return EXIT_SUCCESS;
}

在这里插入图片描述

通过typedef来让p1p2都为char *数据类型

// 2、区分数据类型
void test02()
{
    
    
	//char* p1, p2; // p1是char *  而 p2 是char
	// 通过typedef来区分数据类型
	typedef char* PCHAR;
	PCHAR p1, p2;
	char *p3, *p4; // p3 和 p4都是char *
}

在这里插入图片描述

2.3 提高代码移植性

比如将以下代码放在C89下面去运行,C89不支持long long 类型即无法运行程序,那么就需要将所有的long long类型进行更改为int整型,这样很机械

void test03()
{
    
    
	long a = 10;

	long b = 20;
}

所以为了避免这种类似情况的发生(目前编译器支持都在C99以上标准,只是举例说明),使用typedef后,那么只需要去替换typedef long long MYINT;中的 long long 就可以了

//3、提高代码移植性
typedef int MYINT; //typedef long long MYINT; 只需要替换 long long 就可以了
void test03()
{
    
    
	MYINT a = 10;

	MYINT b = 10;
}

3) void 数据类型

void字面意思是 无类型 void* 无类型指针(万能指针),无类型指针可以指向任何类型的数据。

void定义变量是没有任何意义的,当你定义void a,编译器会报错。

void真正用在以下两个方面:

  • 对函数返回的限定;
  • 对函数参数的限定;

3.1 无类型是不可以创建变量的

// 1、无类型是不可以创建变量的
void test04()
{
    
    
	void a = 10; // error 编译器直接报错,因为不知道给a分配多少内存空间
}

3.2 可以限定函数返回值

void func01()
{
    
    
	return 10;
}

void test05()
{
    
    
	printf("%d\n", func01()); // error %d 是需要整型格式,但是func01方法具有viod类型,所以出错
}
//2、可以限定函数返回值
void func01()
{
    
    
	return 10;
}

void test05()
{
    
    	
	func01(); // 即使可以编译过去,但是会给出一个警告
	//printf("%d\n", func()); // error %d 是需要整型格式,但是func方法具有viod类型,所以出错
}

在这里插入图片描述

3.3 可以限定函数参数列表

调用无参函数时,传递参数

int func02()
{
    
    
	return 10;
}
void test06()
{
    
    	
	func02(10, 20);  // 编译成功,无报错和警告,这也是c语言中存在不严谨的地方之一
}

函数参数为void

//3、限定函数参数列表
int func02(void)
{
    
    
	return 10;
}
void test06()
{
    
    	
	func02(10, 20); // 编译成功,无错误,但是有警告,会提示我们这里其实存在问题
}

在这里插入图片描述

3.4 可以作为万能指针类型

打印void *万能指针的大小

//4、void *  万能指针
void test07()
{
    
    
	void * p = NULL;
	printf("size of void *   = %d\n", sizeof(p));  // 4个字节
}

不同数据类型的指针之间赋值

//4、void *  万能指针
void test07()
{
    
    
	void* p = NULL;
	printf("size of void *   = %d\n", sizeof(p));  // 4个字节

	int* pInt = NULL;
	char* pChar = NULL;

	pInt = pChar; // 编译无误,会警告 “char *”到“int *”的类型不兼容
}

在这里插入图片描述

不同数据类型的指针之间赋值,需要进行强制转换才不会出警告

//4、void *  万能指针
void test07()
{
    
    
	void* p = NULL;
	printf("size of void *   = %d\n", sizeof(p));  // 4个字节

	int* pInt = NULL;
	char* pChar = NULL;

	//pInt = pChar; // 警告 “char *”到“int *”的类型不兼容
	pInt = (int *)pChar;
}

万能指针可以不通过强制类型转换,就可以转为任意类型的指针

//4、void *  万能指针
void test07()
{
    
    
	void* p = NULL;
	printf("size of void *   = %d\n", sizeof(p));  // 4个字节

	int* pInt = NULL;
	char* pChar = NULL;

	//pInt = pChar; // 警告 “char *”到“int *”的类型不兼容
	pInt = (int *)pChar; // 不同数据类型的指针之间赋值,需要进行强制转换才不会出警告
	pInt = p; // 万能指针可以不通过强制类型转换,就可以转为任意类型的指针
}

4) sizeof 操作符

sizeofc语言中的一个操作符,类似于++--等等。sizeof能够告诉我们编译器为某一特定数据或者某一个类型的数据在内存中分配空间时分配的大小,大小以字节为单位。

基本语法:

sizeof(变量); sizeof 变量; sizeof(类型);

sizeof 注意点:

  • sizeof返回的占用空间大小是为这个变量开辟的大小,而不只是它用到的空间。和现今住房的建筑面积和实用面积的概念差不多。所以对结构体用的时候,大多情况下就得考虑字节对齐的问题了;
  • sizeof返回的数据结果类型是unsigned int
  • 要注意数组名和指针变量的区别。通常情况下,我们总觉得数组名和指针变量差不多,但是在用sizeof的时候差别很大,对数组名用sizeof返回的是整个数组的大小,而对指针变量进行操作的时候返回的则是指针变量本身所占得空间,在32位机的条件下一般都是4。而且当数组名作为函数参数时,在函数内部,形参也就是个指针,所以不再返回数组的大小;

4.1 sizeof 基本用法

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// sizeof是不是一个函数?
// 1、本质不是一个函数, 是一个运算符,如 + - * /
void test08()
{
    
    
	// sizeof在统计类型的时候,是需要添加小括号的
	printf("sizeof int = %d\n", sizeof(int));

	// sizeof在统计变量的时候,是可以不加小括号的
	double d = 3.14;
	printf("sizeof d = %d\n", sizeof d);
}

int main()
{
    
    	
	test08();
	system("pause");
	return EXIT_SUCCESS;
}
输出结果
sizeof int = 4
sizeof d = 8

4.2 sizeof 结果类型

首先在验证sizeof返回结果是unsigned int类型之前,我们先看一下无符号和有符号类型的运算结果。

// 2、sizeof返回值是什么? unsigned int
void test09()
{
    
    
	unsigned int a = 10; // 如果一个unsigned int 和 int进行运算,那么会将结果统一转换为 unsigned int 类型
	if (a - 20 >0)
	{
    
    
		printf("结果大于0\n");
	}
	else
	{
    
    
		printf("结果小于0\n");
	}
}

int main()
{
    
    	
	//test08();
	test09();
	system("pause");
	return EXIT_SUCCESS;
}
运算结果
结果大于0

通过以上判断结果,我们就可以去验证sizeof的返回类型是否是unsigned int类型了

void test09()
{
    
    
	if (sizeof(int)-5 > 0)
	{
    
    
		printf("结果大于0\n");
		printf("%u\n", sizeof(int) - 5);
	}
}

在这里插入图片描述

4.3 sizeof 其他用法

统计数组占用内存空间大小

// 3、sizeof其他用法
// 统计数组占用内存空间大小
void test10()
{
    
    
	int arr[] = {
    
     1,2,3,4,5,6,7,8,9,10 };
	printf("sizeof(arr) = %d\n", sizeof(arr));  // 40  int类型4*10个元素;
}

说明一点,数组名传入函数作为形参后,会退化为一个指针,指针指向数组第一个元素的地址

void calcArray(int arr[])  // 当数组名传入到函数中,会被退化成一个指针,指向数组中第一个元素的地址 int arr[] 就等价于 int *arr
{
    
    
	printf("calcArray->sizeof(arr) = %d\n", sizeof(arr)); // 4

}
// 3、sizeof其他用法
// 统计数组占用内存空间大小
void test10()
{
    
    
	int arr[] = {
    
     1,2,3,4,5,6,7,8,9,10 };
	printf("sizeof(arr) = %d\n", sizeof(arr));  // 40  int类型4*10个元素;
	calcArray(arr);
}

在这里插入图片描述

5) 数据类型总结

  • 数据类型本质是固定内存大小的别名,是个模具,C语言规定:通过数据类型定义变量;
  • 数据类型大小计算(sizeof);
  • 可以给已存在的数据类型起别名typedef
  • 数据类型的封装(void 万能类型);

变量

1) 变量的概念

既能读又能写的内存对象,称为变量;
若一旦初始化后不能修改的对象则称为常量。

变量定义形式: 类型 标识符, 标识符, … , 标识符

2) 变量名的本质

  • 变量名的本质:一段连续内存空间的别名;
  • 程序通过变量来申请和命名内存空间 int a = 0
  • 通过变量名访问内存空间;
  • 不是向变量名读写数据,而是向变量所代表的内存空间中读写数据;

3) 修改变量的两种方式

3.1 直接修改

void test11()
{
    
    
	// 1、直接修改
	int a = 10;
	a = 20;
	printf("a = %d\n", a); // 20
}

int main()
{
    
    
	test11();
	system("pause");
	return EXIT_SUCCESS;
}

3.2 间接修改

void test11()
{
    
    
	// 1、直接修改
	int a = 10;
	a = 20;
	printf("a = %d\n", a); // 20
	// 2、间接修改
	int* p = &a;
	// *p 表示解引用
	*p = 30;
	printf("a = %d\n", a); // 30
}

3.3 自定义数据类的修改

struct MyStruct
{
    
    
	char a;
	int b;
	char c;
	int d;
};

void test12()
{
    
    
	struct MyStruct m1 = {
    
     "a", 10, "b", 20 };
	// 直接修改d属性
	m1.d = 200;
	printf("m1.d = %d\n", m1.d); // m1.d = 200
	// 间接修改d属性
	struct MyStruct* p = &m1;
	p->d = 2000;
	printf("p->d = %d\n", p->d); // p->d = 2000
}

以上间接修改d属性的最简单的方式,我们还可以通过步长来找到d属性在内存中的位置;首先先来看看struct MyStruct结果体的大小

void test12()
{
    
    
	struct MyStruct m1 = {
    
     "a", 10, "b", 20 };
	// 直接修改d属性的值
	m1.d = 200;
	printf("m1.d = %d\n", m1.d); // m1.d = 200
	struct MyStruct* p = &m1;
	p->d = 2000;
	printf("p->d = %d\n", p->d); // p->d = 2000
	printf("%d\n", sizeof(struct MyStruct)); // 16
}

打印出struct MyStruct结果体的大小为16字节,16字节是如何计算的,char a;从首地址0开始到哪里看下一个int b;int b从几开始必须为int类型的整数倍也就是4,那么char a;就占0~3,直接全部都给了char a;因为遵循内存对齐的方式,所以后面的3个都没有用;int b;4个字节,所以为4~7,依次类推。

struct MyStruct
{
    
    
	char a; // 0~3
	int b; // 4~7
	char c; // 8~11
	int d; // 12~15
};

那么struct MyStruct0~15范围,占16个字节

在这里插入图片描述

知道结构体的大小后,那么p+1就跨过整个结构体了,打印验证pp+1地址差值

printf("p = %d\n", p);
printf("p+1 = %d\n", p+1);

在这里插入图片描述

struct MyStruct类型的指针显然无法通过步长来指向d属性,那么可以定义char类型指针,一个一个的跳,因为我们目前知道d属性位置是从12开始的,那么char类型指针变量p+12就行了,但是要注意:因为p+12为char*类型,而d属性为int类型,如果不强转只能取一个字节的数据,所以需要强转后再解引用*

void test12()
{
    
    
	struct MyStruct m1 = {
    
     "a", 10, "b", 20 };
	// 直接修改d属性的值
	m1.d = 200;
	printf("m1.d = %d\n", m1.d); // m1.d = 200
	//struct MyStruct* p = &m1;
	//p->d = 2000;
	//printf("p->d = %d\n", p->d); // p->d = 2000
	//printf("%d\n", sizeof(struct MyStruct)); // 16
	//printf("p = %d\n", p);
	//printf("p+1 = %d\n", p+1);
	char* p = &m1;
	printf("*(int *)(p + 12) = %d\n", *(int *)(p + 12));  // 因为p+12为char*类型,而d属性为int类型,如果不强转只能取一个字节的数据,所以需要强转后再解引用*
}

在这里插入图片描述

同理,如果是先将char类型指针p强转为int *类型,那么p+3步长就可以达到开始地址为12d属性,所以指针类型的不同,跳跃的步长是不一样的,你想访问哪个位置的属性,可以自己来进行控制,前提是对内存地址的足够了解。

char* p = &m1;
printf("*(int *)(p + 12) = %d\n", *(int *)(p + 12));  // 因为p+12为char*类型,而d属性为int类型,如果不强转只能取一个字节的数据,所以需要强转后再解引用*
printf("*((int *)p+3) = %d\n", *((int *)p+3));

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_41782425/article/details/128210029