C语言遐想(瞎想)

作者: 01_Jack | 来源:发表于2019-11-07 19:55 被阅读0次

导读

本文为笔者对C语言的一点思考,内容较杂,难免出错。如果阅读过程中发现什么问题,望不惜赐教。推荐顺序阅读,否则可能出现断片现象。

全文共包含以下几部分:

  • 变量与地址
  • 内存空间的申请
  • 变量类型
  • 编码方式
  • 字符型数组与字符串
  • \0作为字符串结束符的必要性
  • 数组、指针、数组指针及指针数组

变量与地址

先说下变量与变量名,这两者的区别可以简单理解为:定义的时候叫变量名,使用的时候叫变量。

int a = 1;    // 变量名
a = 2;    // 变量
printf("%d\n", a);    // 变量

如上所示,可以对变量a进行读写操作,那么变量a的数据存储在哪里?计算机如何找到a存储的位置?

一个程序的运行需要加载到内存中,而程序就是函数与变量的集合,计算机会为每个加载到内存中的函数与变量分配内存,被分配的内存所在的位置就是内存地址。计算机通过对变量a寻址就可以找到a存储数据的位置,读取地址存储的内容就可以获取变量a存储的数据。

C语言通过&符号对变量寻址,通过*符号对指针变量读址

int *p = &a;    // 获取变量a的地址并赋值给指针变量p
*p = 3;    // 读取指针变量p所存储的地址,并对这个地址所存储的内容赋值
printf("%d\n", a);    // 此时a的值为3

可见变量的读写实际是对内存的读写,而变量名可以辅助我们找到存储变量所在内存的地址,内存地址可以辅助我们操作内存。试想一下,如果没有变量名,只能通过地址操作内存,每次要写一堆的0x123456789abc之类的代码这太难受了。

通过变量名找到内存地址,通过内存地址操作内存,这条线可以简单的类比为:通过姓名找到身份证号,通过身份证号找到具体个人。

有了内存地址就可以正确的读写数据吗?最直接的问题,除非计算机只读取当前地址所在字节存储的内容,但是当存储内容大于1 byte时,计算机并不知道读到什么位置结束。所以当我们为变量申请内存空间时,需指明要申请空间的大小。

那么如何为变量申请内存空间?

内存空间的申请

内存空间的申请分为两种:静态申请与动态申请。

两者的区别在于:
静态申请内存空间,变量存储在栈上,当变量生命周期结束时,内存空间自动释放。动态申请内存空间(除alloca外,使用alloca函数申请的内存空间在栈区,无需手动释放),变量存储在堆上,当变量生命周期结束时,内存需手动释放,否则内存泄漏。

如上文中的int a = 1就属于静态申请内存空间。C语言中可以通过,malloc、calloc、realloc等函数动态申请内存空间。

void test1() {
    printf("----%s----\n", __func__);
    int a = 1;
    int *p = &a;
    printf("a的地址:%p\n", &a);
    printf("p的值:%lx\n", (u_long)p);
    printf("p的地址%p\n", &p);
}

void test2() {
    printf("----%s----\n", __func__);
    int *p = (int *)malloc(sizeof(int));
    *p = 1;
    int a = *p;
    printf("a的地址:%p\n", &a);
    printf("p的值:%lx\n", (u_long)p);
    printf("p的地址:%p\n", &p);
    int *q = (int *)malloc(sizeof(int));
    printf("q的地址:%p\n", &q);
    free(p);
    free(q);
}

int main() {

    test1();
    test2();
    return 0;
}
内存空间的申请-1

test1中,变量a与指针变量p都属于静态申请内存空间,两者都存储在栈区,可自动释放。由于p晚于a申请内存空间,所以p位于栈顶,a位于栈底,p的地址小于a的地址(栈顶地址小于栈底地址)。此时将指针变量p的值与变量a的地址相同,因为p本来就存储着a的地址。

test2中,指针变量p与q属于动态申请内存空间,存储在堆区,需手动释放内存。由于q晚于p申请内存,所以堆区中q的地址大于p的地址。变量a属于静态申请内存空间,位于栈区,可以自动释放,所以a的地址小于p的地址(栈区地址小于堆区地址)。

test2中,指针变量p的值为存储着数据1的内存地址,可以通过下面的例子来验证:

u_long test3() {
    printf("----%s----\n", __func__);
    int *p = (int *)malloc(sizeof(int));
    *p = 99;
    return (u_long)p;
}

int main() {
    u_long t = test3();
    int *p = (int *)t;
    printf("%d\n", *p);     // 99
    free((void *)t);
    return 0;
}

注意,这里之所以将指针强转成数值,是为了更好的理解指针内部存储的本来就是数值,只不过这个数值是某个变量的地址。显然,test3中的指针变量p未释放,所以函数体外仍然可以获取指针。

变量类型

所谓变量类型其实就是对变量进行种类划分,如同狗分为藏獒、阿拉斯加、哈奇士等不同品种。C语言中除掉存在char、int、float、double等基本数据类型外,还存在enum、struct、union、指针、数组等类型。如果强转变量类型,可能会造成数据丢失、错误甚至crash。

int main() {
    short *p = (short *)malloc(sizeof(int));
    *p = 999;
    char *q = (char *)p;
    printf("%d\n", *p);     // 999
    printf("%d\n", *q);     // -25
    return 0;
}

999的二进制数据为0000_0011_1110_0111,-25的二进制数据为1001_1001。由于计算机以补码存储数据,所以计算机中存储的数据为1110_0111,这与999二进制数据的后8位相同。显然,小端模式下将存储着999数据的short *类型指针转成char *类型指针后,读取的数值为-25。

同一串二进制数据切割后,以不同的方式读取(数据类型),所读出的数据显然是不同的。其实,编码方式也是对二进制数据的切割(填补)。

编码方式

int main() {
    char a[] = {-28, -67, -96, -27, -91, -67};
    printf("%s\n", a);      // 你好
    char c = 'x';
    printf("%c\n", c);      // x
    printf("%d\n", c);      // 120
    
    return 0;
}

对于变量c为什么以字符形式输出是x,以数字形式输出是120,想必都能答出ASCII码。但是变量a为什么以字符串形式输出的是你好

再来看另一个例子:

int main() {
    char *s = "你好";
    for (size_t i = 0; i < strlen(s); i++) {
        printf("%d\n", *(s + i));
    }
}
编码方式-1

可见,指针s指向地址的连续空间内,存储的数据就是第一个例子a数组中存储的数据。但是为什么这组数据可以表示你好?这又要回到上文提到的二进制数据切割了,也就是这一小节的主题编码

对应{-28, -67, -96}这组数字,转成二进制为{1001_1100, 1100_0011, 1110_0000},计算机中存储的补码为{1110_0100, 1011_1101, 1010_0000}。也就是说,最终计算机中存储的这组补码数据对应着汉字

我们知道,汉字对应着utf8编码(也存在utf16、utf32等编码方式,但是并不常用)

摘自百科

对应的Unicode为\u4f60,对应着0800~FFFF这个范围,所以占3个字节。转成二进制后变为:0100_1111_0110_0000,套用utf8格式1110_xxxx_10xx_xxxx_10xx_xxxx,从低到高用刚才的二进制填补x,二进制不足位用0填补x,最后得到utf8编码后的数据1110_0100_1011_1101_1010_0000,这与{-28, -67, -96}这组数据计算机中存储的转成二进制后的补码相同。

所谓的编码方式就是将字符串转成Unicode码后,以某种格式重新填补形成的新的二进制数据。

字符型数组与字符串

既然说到字符串,顺便说说字符型数组与字符串:

int main() {
    char a[] = {'0', '1', '_', 'J', 'a', 'c', 'k'};
    printf("----------------\n");
    printf("%p\n", a);
    printf("%lu\n", sizeof(a));
    printf("%s\n", a);
    char b[] = "world";
    printf("----------------\n");
    printf("%p\n", b);
    printf("%lu\n", sizeof(b));
    printf("%s\n", b);
    char c[] = {'h', 'e', 'l', 'l', 'o'};
    printf("----------------\n");
    printf("%p\n", c);
    printf("%lu\n", sizeof(c));
    printf("%s\n", c);
    return 0;
}
字符型数组与字符串-1
  1. 首先以静态方式申请了数组变量a、b、c的内存,他们存储在栈区,因为c晚于b晚于a申请内存,所以c位于栈顶,a位于栈底,c的地址小于b的地址小于a的地址(栈顶地址小于栈底),且a、b、c各占7、6、5个字节,c的地址加5后就是b的地址,b的地址加6就是a的地址。可见,a、b、a是一段连续的内存空间。
  2. 由于字符串以\0作为结束符,所以b占用5+1,即6byte。而a和c是以单个字符形式定义的字符数组,所以a和b仅占用数组个数的字节数,分别为7byte和5byte。
  3. 最后一个问题,为啥a、b输出的是赋值数据,而c输出的是helloworld?c的赋值明明是{'h', 'e', 'l', 'l', 'o'}这个数组。答案其实就是上边1、2两小点的结合,由于c与b是一段连续的内存空间,且c不包含\0字符串结束符,当把b按字符串格式输出时,会在以b地址为起始的连续内存空间寻找\0,所以b输出helloworld。再回头看a数组,因为a处于栈底,当没有\0字符串结束符时,仅输出数组内定义的数组,不会额外增加一个字符用于结束当前数组。

再来看个例子:

int main() {
    char a[] = {'t', 'e', '\0', 's', 't'};
    printf("----------------\n");
    printf("%lu\n", sizeof(a));
    printf("%s\n", a);
    
    char b[] = "te\0st";
    printf("----------------\n");
    printf("%p\n", b);
    printf("%lu\n", sizeof(b));
    printf("%s\n", b);

    return 0;
}
字符型数组与字符串-2

虽然a仍然占用定义的数组个数5byte,但是并未输出全部数组内容,而是到\0字符结束,b同样到\0字符结束,两者均输出te。由于b本身定义为字符串,所以末尾会多出1byte用于添加\0作为字符串结束符,占用6byte。

\0作为字符串结束符的必要性

由于C语言没有专门的字符串类型,当我们通过字符型数组或者字符型指针来定义字符串时,很容易数组或者指针越界,以\0作为字符串结束符虽然会增加1byte的存储量,但是此时可以通过判断当前数组或者指针内容是否为\0来甄别字符串是否结束,从而避免越界情况的发生。

但是,为什么是以\0而不是其他字符作为字符串结束符?\0的ASCII码为0,作为字符输出是空白,此时添加到字符串结尾并不影响字符串本身的显示与使用。同时用\0作为占位符,占位符的副作用相对较小,因为通常我们并不需要输出或显示一个空白字符。但是如上文例子所示,当一个字符串中间包含\0时,字符串输出会在\0除被截断,无法输出完整的原字符串。

如果想输出\0这种字符串,可以这样写\\0:

int main() {
    char a[] = "te\\0st";
    printf("%s\n", a);      // te\0st
    return 0;
}

数组、指针、数组指针及指针数组

  • 一维数组与一级指针
    一维数组与一级指针其实没什么好说的,数组名就是数组的起始地址,并且是直接寻址,而指针则是通过*间接寻址。由于数组名就是数组的起始地址,也可以用*数组名的组合方式,进行间接寻址(与指针使用相同)。除此之外,数组是数组,指针是指针,两者是不同的类型,仅仅是由于数组名就是数组的起始地址导致的部分使用方式重叠。

  • 一维数组名与一维数组名取地址

int main() {
    int a[3];
    printf("a:%p\n", a);
    printf("a+1:%p\n\n", a + 1);
    printf("&a:%p\n", &a);
    printf("&a+1:%p\n", &a + 1);
    return 0;
}
数组、指针、数组指针及指针数组-1

虽然a与&a的地址相同,但是a+1与&a+1的结果并不同,根本原因在于两者不是同一种类型。
a相当于&a[0],他的类型是int *,a+1表示首地址+sizeof(int),所以a+1在首地址的基础上向后移动4个字节。
&a的类型为int (*)[3],是个数组指针,他指向包含3个int元素的一维数组,&a+1表示首地址+sizeof(a),所以&a+1相当于在首地址的基础上向后移动12个字节

  • 二维数组与数组指针
    如果真正理解了一维数组与一级指针间的关系,二维数组与数组指针间的关系不在话下:
int main() {
    int a[2][3] = {1, 2, 3, 4, 5, 6};
    int (*p)[3] = a;
    printf("%d\n", (*p)[0]);
    printf("%d\n", (*p)[1]);
    printf("%d\n", (*p)[2]);
    printf("%d\n", (*(p + 1))[0]);
    printf("%d\n", (*(p + 1))[1]);
    printf("%d\n", (*(p + 1))[2]);
    return 0;
}
数组、指针、数组指针及指针数组-2

例子中的p是个数组指针,他指向包含3个int元素的一维数组。当把二维数组a的首地址赋值给数组指针p时,显然(*p)[0]到(*p)[2]对应访问的是a[0][0]到a[0][2],由于变量p的类型为int (*)[3],本质是个数组指针,所以p+1相当于一维数组中的&数组名+1。因此p+1移动sizeof(int*3)个数,从而*(p+1)[0]到*(p+1)[2]指向a[1][0]到a[1][2]

  • 数组指针与指针数组

两者的区别很简单:数组指针是个指针,他指向一个数组;指针数组是个数组,他内部存储着一组相同类型的指针;

这两句话慢慢意会吧☺

你可能会有这种疑问,这样不能描述数组指针吗?

数组、指针、数组指针及指针数组-3

很遗憾,看起来好像是一个指针指向了一个数组,然而int *只能表示指针的特性,编译器无法得知指针指向的是数组(反而告诉编译器指向的是int),所以数组指针的正确表达方式只能是类型 (*变量名)[数组个数]这种格式

  • 二维数组与二级指针(真没什么关系)

为了避免混淆,最后再来说一下二维数组与二级指针:

int main() {
    int a[2][3] = {1, 2, 3, 4, 5, 6};
    int (*p)[3] = a;
    printf("%d\n", **p);    // a[0][0]
    printf("%d\n", *(*p+1));    // a[0][1]
    printf("%d\n", *(*(p+1)));  // a[1][0]
    printf("%d\n", *(*(p+1)+1));  // a[1][1]
}
数组、指针、数组指针及指针数组-4

如你所见,仅此而已。这并不表示二维数组对应着二级指针,显然我们定义的p是个数组指针而非二级指针。之所能通过**p这种形式来访问二维数组,可以通过如下伪代码来表示:

*p == int b[3]
int b[3] == a[1][0]
*p+1 == b+1
b+1 == a[0][1]
*(p+1) == &b+1
&b+1 == a[1][0]
*(p+1)+1 == &b+1+1
&b+1+1 == a[1][1]

最根本原因就在伪代码的第一行,*p指向包含3个int元素的一维数组,而数组名又可以当做地址进一步访问。虽然如此,二维数组与二级指针仍然没有一毛钱关系。


Have fun!

相关文章

  • C语言遐想(瞎想)

    导读 本文为笔者对C语言的一点思考,内容较杂,难免出错。如果阅读过程中发现什么问题,望不惜赐教。推荐顺序阅读,否则...

  • 遐想(瞎想)

    每天三点一线的生活,规律极了。固定的时间踏出家门,固定的时间回到自己的小窝,好像每天都在重复着昨天的故事。 ...

  • 瞎想遐想

    在不知道名字的街想到一些无法忘记的事情 都江堰外的古城,在去寻找那家花店的路上 我在她的皱纹里行走,她的双目紧闭 ...

  • 遐想?瞎想?

    1 灯光璀璨、舞台亮丽;悠扬的歌声从耳边响起,抑扬顿挫,像骑在奔驰的骏马上放歌草原,上下起伏,听的令人陶醉。 我也...

  • 遐想,瞎想

    离开厦门快两年了,每当回想起厦门就会想起BRT莲坂站,想起张宇《小小的太阳》;想起《小小的太阳》便又想起BR...

  • 遐想——瞎想

    我们总是在自己的悲伤中痛苦,而忽略生命本来的光明。 我们有展示的自由,却不想,这是使得我们变得更加平庸的地方。生命...

  • 遐想瞎想

    世界上好多都是矛盾,发现问题,然后解决问题,在发现在解决乐此不疲,没有答案的题也有很多,其实细想也没什么意思,但是...

  • 遐想?瞎想!

    本文纯属吹牛,切勿较真,博君一笑足矣。 老婆的一个女同事离岗创业,到南方发展,科室全体女同事决定举行宴会为她送行。...

  • 瞎想&遐想~

    这个世界是一样的吗?是,也不是。 是,是因为世界是客观存在的万物组成的,不管你生或死、喜或悲,它都那么...

  • 遐想,瞎想

    昨天的雨,很像那么回事,认认真真下了一天。久旱逢甘霖,干渴的草木庄稼应该饱饮了一顿,每一个叶片和枝条都是格外舒展的...

网友评论

    本文标题:C语言遐想(瞎想)

    本文链接:https://www.haomeiwen.com/subject/sdgybctx.html