在 java 语言中,万物皆对象
在 Linux 中,万物皆文件
而在 C 和 C++ 中,万物皆置针
C 语言
C 语言为面向过程的语言,在函数加载的时候,main 函数作为程序入口,应在文件的最后进行声明
C 语言不允许函数重载,java 和 C++可以
.h 文件为声明文件;.c 文件为实现文件(在c++ 中,为.cpp)
#include 引入其他文件,其中,尖括号(<>)引入的,是 C 语言库中的模块, 双引号("")引入的,是用户实现的声明文件
基础类型:int、long、float、double、char 等,值得注意的是,没有string,只有char[]
对应的占位符:%d、%ld、%f、%lf,%c 等,可以直接打印 char[],其占位符为%s;地址占位符为 %p
// C 语言中,没有所谓的 string 类型,字符串的定义用的是 char[]
char[] str = "abc"
指针
定义变量时,会给变量开辟一个内存空间地址。
指针是一类数据类型,具体对应的是数据的内存地址
// 定义了一个 int 类型的变量 a
// 将 a 赋值为 1
int a = 1;
// 定义了一个 int * 类型的变量 ap
// 将 ap 赋值为 变量 a 的存储地址
int *ap = &a;
// 定义了一个 int 类型变量 b,开辟了新的空间地址
// 将变量 a 值,赋值给 b
int b = *ap;
// 定义了一个 int 类型变量 c,开辟了新的空间地址
// 将地址 ap 中储存的值,赋值给 c
int c = a;
// 将 ap 地址对应的位置,修改为 2,即 a 修改为 2
// 而 b 和 c 的地址是新开辟的,所以修改 ap 地址对应位置,并不会修改 b 和 c 的值
*ap = 2;
其在内存映射中的图示大致为:
在函数变量传参时,会重新定义一次入参,所以如果需要通过函数修改变量的值,需要给函数传入对应的变量地址,而不是直接传入一个变量
void change(int c, int d) {
int temp = c;
c = d;
d = temp;
}
int main(int argc, const char * argv[]) {
int a = 1;
int b = 2;
change(a, b);
return 0;
}
相当于:
int main(int argc, const char * argv[]) {
int a = 1;
int b = 2;
int c = a;
int d = b;
// 修改的是新开辟出存储空间的 c 和 d 的值,并不会影响到 a 或者 b
int temp = c;
c = d;
d = temp;
// 运行结果为:a: 1, b: 2, c: 2, d: 1
// printf("a: %d, b: %d, c: %d, d: %d \n", a, b, c, d);
return 0;
}
采用指针作为函数参数:
void change2(int *c, int *d) {
int temp = *c;
*c = *d;
*d = temp;
}
int main(int argc, const char * argv[]) {
int a = 1;
int b = 2;
change2(&a, &b);
return 0;
}
相当于:
int main(int argc, const char * argv[]) {
int a = 1;
int b = 2;
// 将 a 的地址赋值给 c
int *c = &a;
int *d = &b;
// 将 c 地址存储的数据取出,并赋值给 temp
int temp = *c;
// 修改 c 地址存储的值,即修改了 a 的值
*c = *d;
*d = temp;
// 运行结果: a: 2, b: 1, c: 2, d: 1
// printf("a: %d, b: %d, *c: %d, *d: %d \n", a, b, *c, *d);
return 0;
}
多级指针
当一个指针变量指向另一个指针变量时,例如在上述的例子中,int 类型指针变量(int *) ap 指向了 a;新建另一个新的int * 类型指针变量(int **) app,指向指针变量 ap,此时 app 则被称为二级指针。实际开发中,一般不会有超过三级指针的多级指针。
// 新建 int 类型变量 a,赋值为 0
int a = 0;
// 新建 int * 类型变量 ap,赋值为 a 的地址
int *ap = &a;
// 新建 int ** 类型变量 app,赋值为 ap 的地址
int **app = ≈
// 新建 int *** 类型变量 appp,赋值为 app 的地址
int ***appp = &app;
// 不能用以下代码对多级指针赋值
// 因为指针是指向具体内存地址的变量,在 &(&(&a)) 中,只有 a 有具体内存地址值,&a 操作可成立
// 但 &a 操作的值没有另外开辟内存空间进行存储,所以无法计算 &(&a)
// int ***appp = &(&(&a));
printf("a: %d, ap: %p, app: %p, appp:%p\n", a, ap, app, appp);
printf("&a: %p, &ap: %p, &app: %p, &appp: %p\n", &a, &ap, &app, &appp);
printf("a: %d, *ap: %d, *app: %p, *appp: %p", a, *ap, *app, *app);
在指针变量的定义时,类型变量后面添加了多少个 *,即代表指针的层级,指针的层级决定这个指针需要进行几次取值操作后,方能获取最开始存储的数据。其内存示意图,大致如下:
& 操作 -> 获取变量的地址
* 操作 -> 对指针变量的值进行寻址,并获取该地址上的内容
注意: * 操作可以叠加,而 & 操作不可以
数组指针
定义数组时,C 与 java 不同,[] 写在变量名后,而不是类型后;此外,无法在定义时,将一个数组的内容赋值给另一个数组。数组的拷贝需要用到 memcpy 函数
数组在内存空间内的存储是连续的,因而 a[0] 的位置就是数组的起始位置。
当输出 a 的值时,输出的,其实是 a 的地址。可以理解为在 java 中,直接输出一个对象时,输出的其实也是 hash 值(内存映射值)。
因而:a、&a、&a[0],都是数组的起始地址位置,输出的值相同
在 C 中,没有直接获取数组长度的方法,需要通过 数组的内存大小 除以 数组元素类型的大小 来进行计算,并获得结果
数组 a 作为指针时,可以进行位移运算,但无法进行自增自减操作。(可以将 a 视为 finnal 变量)
可以定义一个专门的指针 (aar)对新定义的指针进行增减操作;
指针加减时,指针每 + 1,输出的指向地址 + 类型占位字节数(char -> 1, int -> 4, long ->8)
在 C 语言中,不一定存在数组越界而崩溃的情况。
根据不同的操作系统,不同的编译器,可能会有不同的表现。
有些可以读取出一个野值(即未知意义的一个内存地址值,这也是为什么有些时候 C 代码很难排查 bug,取出了一个不正确的数值,但没有任何报错,只是最终运行的结果与期望值不同);mac 系统上,运行时会直接抛出异常。
// 定义数组 a
int a[] = {1,2,3,4,1000};
// 输出的三个值相同
printf("a: %p, &a: %p, &a[0]: %p\n", a, &a, &a[0]);
// 计算数组的长度
int aSize = sizeof(a) / sizeof(int);
// 拷贝数组时,以下代码不合法:
// int b[] = a;
// 需使用以下方法:
// 其中,数组的大小,在定义时就必须明确,且其值为元素个数
int b[aSize];
// 拷贝时,将 a 数组的 sizeof(a) 字节拷贝到 b 数组对应的地址中
memcpy(b, a, sizeof(*a));
printf("sizeof(a): %lu, sizeof(int): %lu, sizeof(a)/sizeof(int): %lu\n", sizeof(a), sizeof(int), aSize);
//定义一个指针,指向 a 的起始位置
int *aar = a; // a == &a == &a[0]
for (int i = 0; i < aSize; i ++) {
// a 可以进行运算,得出遍历的下一个元素值
// 输出 a + i 时,可发现,a 每 + 1,地址 + 4。因为 int 占位 4字节
printf("*(a + i): %d, a + i: %p\n", *(a + i), a + i);
// 需要对地址进行位移运算后,再进行指针取值运算,需要将 a + i 用括号括起
// 如果没有括起的话,则是取出了第一个元素的值,进行了值的加运算后再输出
// 在 1,2,3,4,5 这样的数组中,输出一致,但意义不对,是 bug
// printf("test: %d\n", *a + i);
// 可以通过 a[i] 的方式,取出对应位置元素值
printf("a[i]: %d\n", a[i]);
// 也可以通过取出自增指针对应位置的元素值,来遍历数组
printf("aar: %p\n", aar ++);
}
// 以下代码,在 xcode 中运行时报错
// printf("a[1000]: %d", a[1000]);
// a[1000] = 20;
函数指针
C 语言中,函数在传参时,会给基础类型开辟新的存储空间,而复杂的结构体,则会传递结构体指针。
数组属于结构体,传递的是指针。
即便参数用的是 int a[] 类型,实际传递的也是 int *a。
int size = sizeof(a) / sizeof(*a);
在定义 a 数组的结构体中,得到的值是数组的长度, sizeof(a)
能成功获取到 a 数组的字节数;而在参数为 int a[] 的函数体内,得到的值为 1,此时 sizeof(a)
相当于 sizeof(*a)
,因为在函数参数中, int a[] 的实质为 int *a(int 指针)类型的变量。
函数也是一种复杂结构,因而直接使用函数名 和 直接使用数组名 一样,获取到的,都是对应的指针位置。
函数指针作为 函数参数时的定义结构:{函数参数返回值类型} (*{函数体中使用的函数参数别名})({函数参数的入参类型,用逗号隔开})
// 第一种打印
// 等同于 void printArray1(int *a, int aSize, char *name)
void printArray1(int a[], int aSize, char *name) {
int *aar = a;
// 在函数体内,无法获取数组类型参数的长度,因为此参数的实质是指针
// int size = sizeof(a) / sizeof(*a);
int i;
for (i = 0; i < aSize; i ++) {
printf("%sar: %d\n", name, *(aar ++));
// 在此修改数组内元素的值。此处修改,会改变原来数组内的值。即其他方法打印与此方法一致。
// 从第二个元素开始,将值改为第几个元素
* aar = i + 2;
}
}
// 第二种打印
void printArray2(int *a, int aSize, char *name) {
int i;
for (i = 0; i < aSize; i ++) {
printf("%s[i]: %d\n", name, a[i]);
}
}
// 必须在 main 之前先声明函数,但实现可以在 main 之后进行
// 第三种打印
void printArray3(int *a, int aSize, char *name);
// 打印数组的函数,可以将具体函数作为参数传递
// void (*method)(int[], int, char *) 为第一个参数,类型为:函数地址,其中:
// 函数参数返回值类型:void;函数体中使用的函数参数别名:method;
// 函数参数的入参类型,用逗号隔开:int[], int, char *
void print(void (*method)(int[], int, char *), int a[], int aSize, char * name) {
// 取出 method 地址对应的函数,并调用
(*method)(a, aSize, name);
// 由于在参数中已声明 method 是一个函数指针,因而 * 可省略,等价于:
// method(a, aSize, name);
}
int main(int argc, const char * argv[]) {
int a[] = {7,2,3,4,1000};
int size = sizeof(a) / sizeof(*a);
// 采用不同的打印方法打印数组
print(printArray1, a, size, "a");
print(printArray2, a, size, "a");
print(printArray3, a, size, "a");
return 0;
}
// 在 main 函数之后实现之前声明的函数
void printArray3(int *a, int aSize, char *name) {
int i;
for (i = 0; i < aSize; i ++) {
printf("*(%s + i): %d\n", name, *(a + i));
}
}
C语言内存地址划分
指针是内存地址的映射,因而,在进一步了解指针前,先对 C 语言的内存划分有个大致概念:
常量(1、“ABC”)、static 修饰的静态变量,会在 常量区 进行存储,由操作系统在程序的运行结束的时候进行销毁
当函数运行时,通过int a = 1;
, char str[] = "abc"
等 静态开辟 的临时变量会在 栈区 开辟存储空间;当函数运行结束后,对应的栈区变量会被销毁。
通过 malloc 动态开辟 的变量属于 堆区 变量、需要手动调用 free进行销毁
当创建变量时,内存分配如下图所示:
Note:字符串类型常数,在存储时,会自动以 '\0' 结尾,表示字符数组结束
C 语言内存四区
-
全局区
该区域由操作系统管理,在程序结束后,由操作系统进行释放。
全局区存放了:在函数外部定义的全局变量(全局变量区)、static 修饰定义的全局静态变量和局部静态变量(静态变量区) 以及 由const修饰的全局变量和字符常量(常量区)
常量区内存放的数据不可更改,const修饰的局部变量可通过指针更改,但它是局部变量,不在全局常量区内。
// 以下代码中,常量 "abc" 和 常量 "abcdef" 在所有函数里面打印出来的地址都一致
char *getString1() {
char *test1 = "abcdef";
printf("string1 abc: %p, test1: %p\n", &"abc", test1);
return test1;
}
char *getString2() {
char *test2 = "abcdef";
printf("string2 abc: %p, test2: %p\n", &"abc", test2);
return test2;
}
int main(int argc, const char * argv[]) {
char *test1 = getString1();
char *test2 = getString2();
printf("test1 in main: %p, test2 in main: %p\n", test1, test2);
char *test3 = "abcdef";
printf("string3 abc: %p, test3: %p\n", &"abc", test3);
return 0;
}
-
栈区
定义在函数内部的 静态开辟 的变量都存放在栈区
静态开辟操作:int a、float b、char c、char string[] 等 直接定义一个类型变量
栈区内空间大小有限,不同的操作系统平台限制不同,不提倡在栈内开辟超过1M的变量空间
栈区的变量,在函数入栈(开始执行)时创建;在函数出栈(执行完毕)后自动释放
char *getString1() {
// char test1[] 会在栈区开辟一块内存地址,并赋值为 "abc"
// 而 char *test1 = "abc" 则会直接取 "abc" 常量的地址
char test1[] = "abc";
// 此处打印正常,test1 的值为 "abc",地址由系统分配
printf("test1 in getString1: %s, address: %p\n", test1, test1);
return test1;
}
int main(int argc, const char * argv[]) {
// getString1 的函数出栈后,函数内的局部变量会被释放
// 因而此处 test1 的地址与 getString1 内打印地址相同
// 但取出来的字符为乱码(空间被释放)
char *test1 = getString1();
printf("test1 in main: %s, address: %p\n", test1, test1);
return 0;
}
-
堆区
堆区由程序员手动申请(malloc)和释放(free),称为动态开辟
只申请不释放会造成内存泄漏。
释放后,需要将对应指针置空,否则会出现悬空指针。
空间释放只进行一次,否则可能会崩溃(不同编译平台和操作系统平台可能会有不同处理)
在 mac 中,栈区的空间被释放后再进行读取,会出现乱码;而堆区的空间被释放后进行读取,不会乱码;但如果被释放的空间被重新分配过,会读取到错误数据。
可以使用 realloc 对原本申请了的区域进行扩充,如果紧接着的内存区域有足够的空闲,会直接在原有的地址延伸空间大小;如果没有足够的空闲,会重新开辟一个足够大的地址,把原先的数据拷贝到新的地址,并释放原先地址,返回新的地址。
char *getString1(const char* string, int index) {
// 实际开发中不要这么分配空间大小,可能不足,也可能盈余
char *test1 = (char *) malloc(20);
if (test1) {
// 拼接字符串,填充申请的空间
sprintf(test1, "%s%d", string , index);
}
return test1;
}
int main(int argc, const char * argv[]) {
int i = 1;
const char *string = "string";
char *p = getString1(string, i);
if (p) {
printf("%d, get string from heap: %s, address %p\n", i, p, p);
// 错误的释放内存方式:
free(p);
// 在 free 之后,紧接着,应该把相关的指针置为 NULL,否则出现悬空指针
// 悬空指针可能导致未知错误
// p = NULL;
// 重新申请一次堆内存,因为 p 已经被释放,所以这里重新申请的内存,很可能与 p 的内存一致
char *p2 = getString1(string, i + 1);
// 如果 p2 与 p 的内存地址一致,则此处 p 取出了一个异常值 string2(应为string1)
printf("%d, p1 from heap after free: %s, address: %p\n", i, p, p);
printf("%d, p2 from heap after free: %s, address: %p\n", i, p2, p2);
// 释放前,先对指针进行判空处理,防止二次释放
if (p2) {
// free(pointer) 之后,紧接着将 pointer = NULL,防止后续再次使用悬空指针 和 二次释放
free(p2);
p2 = NULL;
}
}
return 0;
}
-
代码区(程序区)
存放代码,将函数题转化为二进制数存放进该区域
C 语言函数的压栈和出栈
C 是面向过程的语言,在程序执行的的时候,从 main 函数开始,将 main 函数压入执行栈,然后调用哪个函数,就将该函数压入栈中,执行完毕后将该函数从栈中移除,并且销毁该函数创建的所有临时变量。
void getStringSize(char *str, int *sp) {
char *cur = str;
*sp = 0;
while (*cur) {
cur ++;
(*sp) ++;
}
}
int main(int argc, const char * argv[]) {
char a[] = "abc";
int size = 0;
getStringSize(a, &size);
printf("size of string a is %d\n", size);
return 0;
}