写在前面的话:
✨如果你觉得本文由写的不对的地方欢迎指正
✨共同学习,共同进步
目录
1. 基本类型占用字节数
本例为64bit CPU
, 现在几乎没有人还在用32bit CPU
了吧~
注意: 数据类型本身
不占用内存空间,只有通过数据类型声明的变量
才占用内存空间
2.数据类型的本质(从数组说起)
2.1 数组参数
数组变量就是数组首地址、也是第一个元素的地址:
int b[10];
printf("b=%d, &b=%d, &b[0]=%d", b, &b, &b[0]);
-------------------
b=5502684, &b=5502684, &b[0]=5502684
也就是说:int *bp
可以指向b
或者&b[n]
, 不太好理解 ?
// int* 表示获取一个有效值需要读取多少位(bit)
void aFstEle(int* arr) {
printf("%d, %d \n", arr[0], *arr);
}
-------------------
...
int a[] = { 15 };
// 这里的 实参a 传递的是第一个元素的地址
aFstEle(a); // 15, 15
...
int b = 15;
// 传递 b 地址
aFstEle(&b); // 15, 15
指针还真是个万金油
?, 还好Java
隐藏了这个该死的特性(就想搞明白一些东西, 特意回头来学习C/C++ ?)
void fun(int ap[]) {
sizeof(ap) // 4
}
void fun(double ap[]) {
sizeof(ap) // 4
}
注意: 不能在数组参数上求数组长度,数组参数会被退化为指针:
2.2 数组的值和地址
下面这个小实验验证数组的数据类型, 与数组指针的区别?
int a[10]= { 15,10 };
// 通过(偏移)数组变量(当前指向)来获取数组值
printf("a=%d, a+1=%d, %d, %d\n",a, a + 1, *(a + 1), (a + 1)[0]);
// a=13630480, a+1=13630484, 10, 10
// 通过(偏移)数组指针(当前指向)来获取数组值
printf("&a=%d, &a+1=%d, %d, diff=%d\n", &a, &a + 1, *(&a + 1), ((int)& a + 1) - ((int)& a));
// &a=13630480, &a+1=13630520, 13630520, diff=1
可以发现 +1
操作其实是将指针从当前位置向后偏移一个类型宽度(sizeof(type)
)。
a+1
:a的类型其实是int *
,他的宽度为sizeof(int)
, a+1等同于a[1]
&a+1
:&a的类型其实是int[N] *
,他的宽度为sizeof(int) * N
,&a+1在这里没有实际意义
对于
&a+1
如果a代表数组指针
数组 中的一个元素,将获取a后面的元素。
这是绕口令吗?
产生如此结果的根本原因在于:a
和&a
虽然都是指针,但数据类型完全不同;
注意: 即使类型宽度
相同,不同类型对二进制的解释方式也是不同。
2.3 数组长度
一直搞不明白数组元素实际类型与数组元素类型他们之间的区别和联系?
double a[] = {3, 2.0};
const int len = sizeof(a)/sizeof(a[0]);
so, 这里有个疑问: a[0]到底是int还是double??
由于不知道在 VS 中 typeof() 该怎么用, 使用了另一种验证方式:
printf("%f", a[0]/2); // 1.500000
printf("%f", 3/2); // 0.000000
这就可以得出一个结论, a[N]
的类型与声明时的类型一致。相当于a[0] = (double)3
。
3. 类型/扩展类型
3.1 类型别名
通过 typedef type typeAlias
定义类型别名,使用别名声明变量 typeAlias varName
// 基本类型
typedef unsigned int u32; // 无符号32bit整型
u32 ua,ub;
// 结构体
typedef struct Table {
int a;
int b;
} taTab;
struct Table tab1; // 默认方式声明结构体变量
taTab tab1; // 通过别名声明结构体变量
3.2 void
void
也叫做无类型
或空类型
,可用于修饰函数参数
和函数返回值
,
注意:不能定义普通类型变量
,但可以定义void *
类型的指针(万能指针)
void func();
void func(void);
void * func();
void * func(void * p);
void * p;
4. 变量
先了解几个问题:
- 数组是不是一种数组类型:是,数组有自己的
类型宽度
- 函数是不是一种数据类型:是,函数名就是函数的入口地址
// 定义了一个指针变量 p,该指针变量可以指向返回值类型为 int 型,且有两个整型参数的函数
// p 的类型为 int(*)(int,int)
int(*p)(int, int);
printf("%d", sizeof(p)); // 4, 所有指针类型的长度都是一样
概念:
变量
:即能读又能写的连续内存空间
的别名
int a, b=10;
char c = 'a';
常量
:初始化后能读不能写的连续内存空间
的别名
const double PI = 3.141592635;
const int ZERO = 0;
为变量赋值
int a;
a = 10; // 直接赋值
int* p;
p = &a; // p指向a的地址
*p = 20; // 把p指向的内存(也就是变量a)的值重新赋值为 20
4.1 内存四区模型
堆区
:heap,通过malloc
、newfree
、delete
来管理。需要手动创建和释放(注意内存泄漏)
栈区
:也叫临时区
,保存程序局部变量。系统自动闯将、释放空间。
全局区
:未初始化、初始化、文字常量区。保存static
和全局变量
代码区
:操作系统自动管理
#include <stdio.h>
char* get_str() {
// char* p = "abcdef"; // p在栈区, "abcdef"在全局区-静态文字区
// char p[] = "abcdef";
// char p[]在堆区分配空间, 并拷贝常量值到数组中, get_str()运行结束后空间自动释放
// 如果在外面引用这个地址, 会导致内存数据不确定性
// 要从函数中返回一块内容可以将内容保存在堆区, 使用完后手动释放
char* p = (char *)malloc(sizeof(char)*128);
if (NULL != p)
strcpy(p, "abcdef");
return p;
}
int main() {
char *p; // p 在栈区
p = NULL; // NULL 宏定义的内容在全局区-静态文字区
p = get_str(); // get_str 在代码区
if (NULL != p) {
printf("%s, %d", p, p);
free(p); // 释放堆空间
}
return 0; // 0在全局区-静态文字区
}
内存地址生长方向
❓ 了解这个有啥好处,目前还不知道 ?
看了这篇文章(为什么栈和堆的生长方向不一样)有个大概的印象
栈:从高地址到低地址生长
堆:从低地址向高地址生长
数组:首地址为低地址,+N
操作可以获取指定(偏移)位置的元素值
结构体:根据声明方式确定保存在哪个区,变量默认保存在栈区,通过malloc()
分配的在堆区
之前一直有个疑惑,为什么
数组
可以从低字节开始。如果数组长度发生变化怎么办?其实想岔了,数组声明后长度就是固定的。虽然数组变量是一个指向首元素的地址,但它是一个常量指针不可以改变指向,所以就不存在长度变化的问题。
注意:
- 如果代码中出现常量内容, 首先区文字常量区查找,如果找到就直接使用,否则就保存新的常量内容。
free()
并不是真正的释放空间, 只是告诉操作系统, 这块空间可以重新利用了,- 主函数声明的任意区的内存空间可以在子函数中使用, 子函数申明的堆空间(未释放时)可以给后面的函数使用。
5. 头文件
// x.h
#pragma once // 多次包含时, 只解析一次
// 防止头文件重复包含
#ifdef __cplusplus
extern "C" { // 使用C编译器 GCC, 如果不写在VS中默认使用 G++ 编译器
#endif // __cplusplus
// 头文件声明
// ...
#ifdef __cplusplus
}
#endif // __cplusplus
6. 打印数据
区别指针变量和指针所指向内容
char *p = &c;
printf("%s || %d", p, p);
%s
:打印p所指向的内存
%d
:打印p本身的值(地址)
字符串以\0
作为结束符,如果字符串中包含\0
后面的内容会被忽略。
疑问:
- 如何获取
\0
之后的有效字符