char远非仅仅是字符

字符的表示

#include <stdio.h>

int main() {
    char c ='A';
    printf("%c", c);
    
    return 0;
}

在书写一个char的字面值时,须用单引号“'”括起来。

signed char, 还是unsigned char?

若用char来表示一个字符,由于不能用负数来表示一个字符,因此符号位则没有意义。因此,表示一个字符的最好的类型是unsigned char

unsigned char c ='A';
printf("%c", c);

一个带有符号的signed char由于需用第1位存储表示正负符号的内容,因此,它只剩下7位的空间来存储有实际意义的内容。而unsigned char可完全使用一个字节的8位来表示其内容。我们可以通过头文件limits.h来显示它们的尺寸大小。

#include <stdio.h>
#include <limits.h>

int main() {
    printf("%13s: (%4d, %4d)\n", "signed char", SCHAR_MIN, SCHAR_MAX);
    printf("%13s: (%4d, %4d)\n", "unsigned char", 0, UCHAR_MAX);

    return 0;
}

输出结果为:

  signed char: (-128,  127)
unsigned char: (   0,  255)

 

注意

由于无符号的类型的最小值总是0,因此limits.h并没有为无符号的数据类型定义最小值的常量。

现在的问题是,如果代码并没有显式地声明signed charunsigned char,只是简单地声明char,那么它到底是signed char还是unsigned char

char c = 'A';

最好的办法是打印出CHAR_MIN以及CHAR_MAX来看看。

printf("%d\n", CHAR_MIN);  // -128
printf("%d\n", CHAR_MAX);  //  127

在这种实现下,char实际上就是signed char,只能表示128种字符。

由于ASCII码中值为128-255的字符是一些图形字符而非文本字符,因此,如果表示编程语言中的字符,signed char够用了。

但我们需知道,各个实现是不一致的,在limits.h中,可看到这样的代码:

#ifdef __CHAR_UNSIGNED__  /* -funsigned-char */
#define CHAR_MIN 0
#define CHAR_MAX UCHAR_MAX
#else
#define CHAR_MIN SCHAR_MIN
#define CHAR_MAX __SCHAR_MAX__
#endif

即,如果定义了__CHAR_UNSIGNED__,则charunsigned char,否则,它就是signed char

char也是整数

虽然char是一个代表字符的符号,但实际上,char是整数类型。我们可以使用printf函数打印出其整数值。

char c ='A';
printf("%d", c);  // 65

这个整数值,其实就是变量c的ASCII码值。

我们可以这样理解,在内存中,char存储为一个整数,而函数printf可以通过格式限定符“%c”的方式把它转换为字符的形式打印出来,通过“%d”,就如实地打印其ASCII码值。

应用一:打印ASCII码值范围是(0, 127)的字符的ASCII码值。

char i;

for (i = 0; i < 128; i++) {
    printf("%c: %d\n", i, i);
}

可以看到,ASCII码值从第33开始,到126的字符,是可打印字符。大写字母A的ASCII码值是65,而小写字母a的ASCII码值是97,之间相差32。利用这一点,可实现大小写的转换。

应用二:大写字符转换为小写字符

char c = 'A';
printf("%c", c + 32);  // a

char在内存中的存储情况

先创建一个工具。

memutils.h的内容:

#ifndef memutils_h
#define memutils_h

#include <stddef.h>

void dump_mem(const void *addr, size_t bytes_per_row, int rows);

#endif /* memutils_h */

memutils.c的内容:

#include "memutils.h"
#include <stdio.h>
#include <ctype.h>

void dump_mem(const void *addr, size_t bytes_per_row, int rows) {
    const unsigned char *pc = (unsigned char *)addr;
    
    const unsigned char *pc_in_row = NULL;
    
    for (int i = 0; i < rows; i++) {
        pc_in_row = pc + bytes_per_row * i;
        
        printf("0x%X", pc_in_row);
        
        for (int j = 0; j < bytes_per_row; j++) {
            printf(" %02X", *pc_in_row);
            pc_in_row++;
        }
        
        printf("    ");
        
        pc_in_row = pc + bytes_per_row * i;
        
        for (int j = 0; j < bytes_per_row; j++) {
            if (!isprint(*pc_in_row)) {
                printf(".");
            } else {
                printf("%c", *pc_in_row);
            }
            pc_in_row++;
        }
        
        printf("\n");
    }

    printf("\n");
}

char是最小的内存单位。其大小是1个字节。1个字节总是由8位组成。

char c ='A';
printf("%lu\n", sizeof(c));  // 1

我们看看其内存情况:

#include "memutils.h"
#include <stdio.h>

int main() {
    char c = 'A';
    dump_mem(&c, 16, 4);
    
    return 0;
}

输出结果:

0xEFBFF45B 41 00 00 00 00 78 F4 BF EF FE 7F 00 00 5D 7F 36    A....x.......].6
0xEFBFF46B 20 FF 7F 00 00 5D 7F 36 20 FF 7F 00 00 00 00 00     ....].6 .......
0xEFBFF47B 00 00 00 00 00 01 00 00 00 00 00 00 00 78 F6 BF    .............x..
0xEFBFF48B EF FE 7F 00 00 00 00 00 00 00 00 00 00 EF F6 BF    ................

“0xEFBFF45B”即是变量c的内存地址,在该地址处存放了一个值为“41”的值,而“41”实际上是一个十六进制的值,对应十进制“65”,正是字母“A”的ASCII码值。

在上面的地址表中,从第2列到第17列,每一组都是由2位的十六进制的字符组成,十六进制的“FF”等于十进制的“255”,对应于一个字节8位最多所能表示的位数(2^{8} = 255)。因此,上面这些列实际上就是一个字节。

函数dump_mem(&c, 16, 4)的意思是从变量c的内存地址开始,打印一系列的内存存储情况,每行打印16个字节,共打印4行。

从这我们可以看到,char在内存中是保存了其ASCII码值这个整数值。

声明两个char的内存情况

char c1 = 0x41;
char c2 = 0x42;

printf("c1 (0x41): 0x%0X\n", &c1);
printf("c2 (0x42): 0x%0X\n\n", &c2);

dump_mem(&c2, 1, 4);

输出结果:

c1 (0x41): 0xEFBFF45B
c2 (0x42): 0xEFBFF45A

0xEFBFF45A 42    B
0xEFBFF45B 41    A
0xEFBFF45C 00    .
0xEFBFF45D 00    .

声明了c1c2两个变量,先直接打印出它们各自的地址,然后再查看它们在一片内存中先后存储的顺序情况。

因为内存地址表是以十六进制表示的,为方便对照,上面声明两个变量的值时也直接使用了十六进制的方式,因为我们并不关心其值,我们只关心每个变量在内存地址表中的实际位置。而内存地址表中最后一列负责打印相应的字符。

上面的函数dump_mem(&c2, 1, 4)每行只打印一字节,这样可方便地看出内存排列顺序。

首先,对于所声明的每个char,编译器都分配了一个地址。这就是我们所说的char是最小的内存单位的意思。

正如您所见,在函数内部,对于先后声明的变量,变量分配地址的顺序是从高位到低位来分配的。

声明一个char的数组

char arr[] = {1, 2, 3, 4, 5, 6, 7, 8};
dump_mem(arr, 16, 2);

输出结果为:

0xEFBFF450 01 02 03 04 05 06 07 08 9F 00 0F 8F F8 29 73 0B    .............)s.
0xEFBFF460 78 F4 BF EF FE 7F 00 00 5D 7F 36 20 FF 7F 00 00    x.......].6 ....

与上面先后声明两个变量的存储顺序不同,对数组分配存储空间的顺序是从低位到高位来排列的。只有这样,才能确保数组的遍历正常。

int element_num = sizeof(arr) / sizeof(arr[0]);

int i;
for (i = 0; i < element_num; i++) {
    printf("%0x\n", arr[i]);
}

现在来看一下使用字符指针来访问内存的情况。

char arr[] = {1, 2, 3, 4, 5, 6, 7, 8};
char *pc = arr;

int element_num = sizeof(arr) / sizeof(arr[0]);

int i;
for (i = 0; i < element_num; i++) {
    dump_mem(pc, 1, 1);
    pc++;
}

声明了一个指向变量arr的指针pc。在for循环中,打印该指针所指向的内存地址及其所存储的十六进制的数值(每行一个字节,即每行一个不同的相邻的地址),然后再将该指针向后移动一个字符的位置,再继续打印。输出结果为:

0xEFBFF450 01    .
0xEFBFF451 02    .
0xEFBFF452 03    .
0xEFBFF453 04    .
0xEFBFF454 05    .
0xEFBFF455 06    .
0xEFBFF456 07    .
0xEFBFF457 08    .

虽说现在来看,效果、意义还是不很明显,但我们还是要先指出:每移动一次字符指针,仅移动一个字节的距离。到下面就更清楚这句话的含义了。

声明一个int的数组

int arr[] = {1, 2, 3, 4, 5, 6, 7, 8};
int *pa = arr;

int element_num = sizeof(arr) / sizeof(arr[0]);

dump_mem(pa, 4, 8);

输出结果:

0xEFBFF430 01 00 00 00    ....
0xEFBFF434 02 00 00 00    ....
0xEFBFF438 03 00 00 00    ....
0xEFBFF43C 04 00 00 00    ....
0xEFBFF440 05 00 00 00    ....
0xEFBFF444 06 00 00 00    ....
0xEFBFF448 07 00 00 00    ....
0xEFBFF44C 08 00 00 00    ....

由于一个int的字长为4个字节,因此可以看到每两个int之间的地址相距为4。

在每个int的存储内存中,由于1-8的数值都较小,只用了1个字节,空出3个字节,所占用的字节位置都排在最前面,后面紧跟着3个数值为“00”的字节。

我们看一下使用两个字节的int的内部存储情况。

int arr[] = {0x1234, 2, 3, 4, 5, 6, 7, 8};
...

输出结果:

0xEFBFF430 34 12 00 00    4...
0xEFBFF434 02 00 00 00    ....
0xEFBFF438 03 00 00 00    ....
0xEFBFF43C 04 00 00 00    ....
0xEFBFF440 05 00 00 00    ....
0xEFBFF444 06 00 00 00    ....
0xEFBFF448 07 00 00 00    ....
0xEFBFF44C 08 00 00 00    ....

我们知道,“0x1234”中的“12”是高位,“34”是低位,但在上面的排列中,从左到右,并非排成“1234”,而是排成“3412”。换个更直观的形式来看看。

...
dump_mem(pa, 1, 16);
...

输出结果:

0xEFBFF430 34    4
0xEFBFF431 12    .
0xEFBFF432 00    .
0xEFBFF433 00    .
0xEFBFF434 02    .
0xEFBFF435 00    .
0xEFBFF436 00    .
0xEFBFF437 00    .
0xEFBFF438 03    .
0xEFBFF439 00    .
0xEFBFF43A 00    .
0xEFBFF43B 00    .
0xEFBFF43C 04    .
0xEFBFF43D 00    .
0xEFBFF43E 00    .
0xEFBFF43F 00    .

这次,每个地址一行,从上到下,内存低端地址向内存高端地址变化。

从整个数组的排列来看,数组的第一个元素排在最低端的内存地址,最后一个元素排在最高端的内存地址,这与其索引值从0到大数变化相吻合。而在一个int的范围内,高位数同样位于高端地址,低位数同样位于低端地址。

把第一元素的4个字节都填满。

int arr[] = {0x12345678, 2, 3, 4, 5, 6, 7, 8};
...

输出结果:

0xEFBFF430 78    x
0xEFBFF431 56    V
0xEFBFF432 34    4
0xEFBFF433 12    .
0xEFBFF434 02    .
0xEFBFF435 00    .
0xEFBFF436 00    .
0xEFBFF437 00    .
0xEFBFF438 03    .
0xEFBFF439 00    .
0xEFBFF43A 00    .
0xEFBFF43B 00    .
0xEFBFF43C 04    .
0xEFBFF43D 00    .
0xEFBFF43E 00    .
0xEFBFF43F 00    .

我们知道,第一个元素的内存地址是“0xEFBFF430”,指向了这个int最低端的字节位置。尽管如此,当我们引用此地址时,编译器知道如何正确地取出该值。

printf("address: 0x%0X\n", &arr[0]);
printf("value: 0x%0X\n", arr[0]);

输出结果:

address: 0xEFBFF430
value: 0x12345678

char指针指向int数组

...
char *pc = arr;

printf("address: 0x%0X\n", pc);
printf("value: 0x%0X\n", *pc);

pc++;

printf("address: 0x%0X\n", pc);
printf("value: 0x%0X\n", *pc);

输出结果:

address: 0xEFBFF430
value: 0x78
address: 0xEFBFF431
value: 0x56

因为char指针只指向一个int的第一个字节的地址,而这个地址中存储的内容是int最低位的内容,因此,希望您对此结果不要有丝毫的诧异。

当然,这种场合并非char指针最合理的用法。char指针最大的价值在于,它可以深入到任何数据类型的内存中,将其每个内存地址都调取出来。它和其他类型的指针是部分与整体的关系。它是表示内存的最小单位。简单而实用的dump_mem函数正是基于此原理而设计。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值