C语言中的指针、数组与字符串详解
本文最后更新于413 天前,其中的信息可能已经过时,如有错误请发送邮件到blue16@email.swu.edu.cn

简介

这一部分,是C语言的重中之重。学习完C语言之后,懂得原理比记得住怎么写代码更重要。因为后面写代码很少遇到C语言(目前用到C的地方是机器人电控代码编写)。但是原理是你精通C++、Java等的基础。

这些内容,如果今天不讲我真的也不会去细看,也算一起进步了。

一些前置知识

存储分类

速度排序:寄存器(register)、L1、L2、L3(CPU三级缓存)、内存、外存(固态)

存储单位

最小的存储单位:比特(bit),表示一个二进制位

规定:1个字节由8个比特位表示,原因和历史发展有关系,这也是char类型由来:

在计算机科学发展的早期,8位(bit)被选为处理字符的合适大小。当时,大多数计算机系统是基于8位的,因为8位足以表示所有的英文字母(大写和小写)、数字、以及其他常用的符号(如标点符号),这些字符集后来被称为ASCII码。

位:英文名称叫做”bit”,表示一个二进制位。32位就是用32个bit表示内存地址,64位就是用64个bit表示内存地址。因此可以简单理解为:”位数=寻址能力”。

内存地址

操作系统为了管理内存,将内存每一个字节编上号,这样每一个号码我们称为内存地址

内存地址=门牌号:也是一个整数(比如0001、0002……),这个整数有多少就表示操作系统能管理多少内存

为了方便表示(十进制太长了),我们惯用十六进制表示内存地址,例如”0x5FFE8C“:0x表示这是个十六进制的数字。

32位与64位

内存地址由整数表示,那这个整数有没有最大上限呢?实际上,这个上限就是计算机能管理的最大内存。要找到这个上限,其实我们只需要知道计算机能够表示的最大整数就行了。

两者最大支持内存

知道了位的概念,就可以作出如下计算:

  • 32位:可以寻址232个不同的地址,即4GB大小的内存。
  • 64位:可以寻址264个内存地址,即16EB(1EB = 1024PB,1PB=1024TB,1TB=1024GB)的内存。

一些俗称

  • x86:x86或80×86是英特尔Intel首先开发制造的一种微处理器体系结构的泛称,为32位系统,因此一般在作为32位系统的代名词。
  • amd64:x64于1999年由AMD设计,AMD首次公开64位集以扩展给x86,称为“AMD64”,因此一般作为64位系统的代名词。

C语言内存模型

C/C++和Java的区别:C/C++可以直接操作系统内存、手动回收、手动释放内存,Java:不能直接操作系统内存(Java代码运行在JVM中,Java Virtual Machine),自动管理内存

Tips:重点掌握堆区栈区。这一部分到数据结构你们会学,谁先学到谁薄纱。

栈区

定义

存放函数的参数值、局部变量等,由编译器自动分配和释放,通常在函数执行完后就释放了,其操作方式类似于数据结构中的

底层实现

  • 一段连续的存储空间
  • 数据结构的栈(数组实现方式)

声明方法

省流:你代码中没有指针的变量定长的数组就是位于栈区的,如下代码所示:

int num = 20;
int nums[20];

上面这两种方式声明的变量存储的位置就在栈区,为什么?因为大小是固定的运行时无法改变,编译器在编译的时候就确定了。

特点

  • 静态:在栈区申请空间的变量都是预先由开发者设定好大小的,编译器分配,程序运行时无法改变
  • 快速:底层实现为数据结构的栈,类似于受限制的数组,由CPU直接管理,非常适合数据的快速读写(例如函数的调用栈也是一个道理)
  • 容量小:栈区空间非常小过大会发生栈溢出,常见错误为:段错误(segmentation fault)或者是缓冲区溢出(Buffer overflow)

堆区

就是通过new、malloc、realloc分配的内存块,编译器不会负责它们的释放工作,需要用程序区释放。分配方式类似于数据结构中的链表。“内存泄漏”通常说的就是堆区。

#include <stdlib.h>
int* ptr = (int*)malloc(sizeof(int) * number_of_elements);
if (ptr == NULL) {
    // 处理内存分配失败的情况
    exit(EXIT_FAILURE);
}
// 使用分配的内存
// 用完后记得释放内存
free(ptr);

堆区的特点:排序二叉树、链表存储(高效利用空间、但是读取速度满(需要寻址)、消耗内存多(需要存储每一个元素的内存地址))

静态区

全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后,由系统释放。

常量区

常量存储在这里,不允许修改。

代码区

顾名思义,存放代码。

指针与变量

很多人会说,指针就是内存地址,其实我之前也是这么理解的,实际上是错的。

在C语言中,指针的理解应该为:指针=内存地址+数据类型

定义指针变量(int*)

阅读下列的代码:

int *pointer_1,*pointer_2;

这两行代码声明了pointer_1和pointer_2是两个指针变量(存放指针的变量)。

取出一个变量的地址(&)

阅读下列的代码:

int var1 = 1,var2 = 2;
int *pointer_1 = &var1,*pointer_2 = &var2;

简单看看,”&”就是取出变量的内存地址,并将地址赋值给指针变量。但实际上这个理解有问题。

让我们对这个代码做一下修改:

int var1 = 1,var2 = 2;
double *pointer_1 = &var1,*pointer_2 = &var2;

对代码进行编译,就会报下面的错误:

error: initialization of ‘double *’ from incompatible pointer type ‘int *’ [-Wincompatible-pointer-types]

如果单单是地址,那么只要是指针变量,都应该能接收,根据报错来看,”&”返回的指针既包括了内存地址,也包括了数据类型

在C语言中,一个变量的指针的含义包括两个方面,一是以存储单元编号表示的内存地址(如编号为2000的字节),一是它指向的存储单元的数据类型(如int,char,float等)。

访问指针(*p)

*:指针运算符(也叫”间接访问”运算符),*p代表指针变量指向的对象。

int a = 1; // 定义一个变量a
int *p = &a; // 取a的地址并用指针变量p存储
*p = 3; // 使用"*"操作符("间接访问"运算符)修改p指向的变量a的值为3

所以有个经典的问题:我能否用int存放指针并操作对应的变量?

答案显然,是不行的。在上文中你可以发现,C语言的指针并不是单一的内存地址,而是包含了数据类型。但是,你可以将一个指针作为整数输出,直观感受什么是指针。(也就是说,这个过程是不可逆的)

#include <stdio.h>
#include <stdlib.h>

int main(){
    int value = 10;
    int *ptr = &value; // ptr指向value的地址
    int *arr = (int*)malloc(sizeof(int)*13);

    // 输出指针变量的地址(即ptr存储的值)
    printf("Pointer address: %X\n", ptr);
    printf("Pointer address: %X\n", arr);
    return 0;
}

指针与二维数组

sizeof运算符

在C语言中,sizeof运算符用于确定变量或数据类型的大小(以字节为单位)。sizeof是一个编译时运算符,它的结果是由所使用平台的字节大小决定的,计算是通过编译器计算的,而不是通过运行后测量的

int arr[10];
printf("Size of array arr: %zu bytes\n", sizeof(arr));
数据类型大小(字节)比特位
char18 bit
int432 bit
float432 bit
double864 bit
long long int864 bit
常见的数据类型及其大小

数组的本质

在C中声明一个数组,实际上这个标识符只记录了数组的第一个元素的指针

数组是一组有序数据的集合,其存储方式是连续的,也就可以通过计算确定每个元素的指针位置并操作它。

指针的运算

指针的加法与减法,实际上是在内存地址的基础上,加上对应数据类型整数倍。例如下面的代码:

int a[10];
*(a+2)=5; //将a中第三个元素赋值为5
a[2]=5; //这两个写法是等效的

等效原因:[]实际上是变址运算符,即将a[i]按a+i计算地址,然后找出此地址单元中的值。这里a+2运算过后得到的新指针地址为:a的首元素内存地址+sizeof(int)*2,数据类型不变,为int。

因此访问数组元素有两种方法:

  • 下标法:a[i]
  • 指针法:*(a+i)或*(p+i)

动态内存分配

根据程序运行状态,动态申请空间,返回的均为首元素的指针。

malloc:申请对应大小的空间

calloc:申请n个对应大小的空间并初始化为0

realloc:重新分配动态存储区。

int* ptr1 = (int*)malloc(10 * sizeof(int)); // 申请10个整型的空间
int* ptr2 = (int*)calloc(10, sizeof(int)); // 申请10个整型的空间并初始化为0
int* ptr3 = (int*)malloc(5 * sizeof(int)); // 初始分配5个整型的空间
int* new_ptr = (int*)realloc(ptr3, 10 * sizeof(int));// 调整内存大小
// 用完了记得释放空间
free(ptr1);
free(ptr2);
free(new_ptr);

字符串

ASCII

在C语言中,字符用char表示,一个字符相当于一个整数,对应的为ASCII表(附录A):

// 一般来说,用char存放字符
char c = 'a';
// 由于字符相当于一个整数,当然也可以通过int存放
int c = 'a';

可以记住几个特殊值,后面用起来方便

ASCII字符
48‘0’
65‘A’
97‘a’

认识字符串

C语言中没有字符串类型,也没有字符串变量,字符串是存放在字符型数组中的。

char c[]={"China"};
printf("%s\n",c);

这段代码中,c的实际存储情况如下:

这里c的长度编译器自动计算了,得出的应该是6。为什么是6呢?因为结尾还有个”\0″,如果使用printf输出,就是识别到”\0″结束输出的,如果没有”\0″,会输出一堆乱码。

你说我非要操作char[9]或者char[10]会怎么样?Java等高级编程语言编译就会报错导致不通过,但是C无所谓,这也就给黑客带来了机会:”缓冲区溢出攻击“

字符串相关函数

格式化(sprintf、fprintf、printf)

  • sprintf:字符数组
  • fprintf:文件流(可以是标准输出、文件等)
  • printf:标准输出(通常是屏幕)

可以用sprintf把信息输出到字符串,用法和printf、fprintf类似。但应当保证字符串足够大,可以容纳输出信息。

sprintf的用法:输出到字符串

#include <stdio.h>

int main() {
    char buffer[100];
    int num = 42;
    sprintf(buffer, "The answer to life, the universe, and everything is %d.", num);
    // buffer now contains the formatted string
    // Use another function to print the buffer, or use it in another context
    printf("%s\n", buffer); // This line is just to demonstrate the content of buffer
    return 0;
}

fprintf的用法:输出到文件

#include <stdio.h>

int main() {
    FILE *file = fopen("output.txt", "w"); // Open file for writing
    if (file == NULL) {
        perror("Error opening file");
        return 1;
    }
    int num = 42;
    fprintf(file, "The answer to life, the universe, and everything is %d.\n", num);
    fclose(file); // Close the file
    return 0;
}

printf的用法:输出到终端(略)

输入与输出(puts、gets、getchar)

  • puts(字符数组):输出一个字符到终端。
  • gets(字符数组):输入一个字符串到字符数组。(现在常用getchar)

gets和getchar:

  • gets 函数用于读取一行文本直到遇到换行符(‘\n’)或EOF(文件结束标志),然后它会丢弃换行符,并用空字符(‘\0’)替换它,作为字符串的结束标志。gets 函数会将读取的字符串存储在由其参数指定的字符数组中。
  • getchar 函数用于读取下一个可用的字符,通常是用户输入的第一个字符,并返回读取的字符作为 int 类型的值。如果遇到文件结束或读取错误,则返回EOF。

注意:

  • gets 函数是不安全的,因为它不检查目标缓冲区的大小,可能会导致缓冲区溢出,这是一个严重的安全漏洞。因此,在新的代码中不应使用 gets 函数。
  • getchar 是安全的,因为它一次只读取一个字符,不会导致缓冲区溢出。

getchar读取整个文本(包括空格回车),核心是判断是否到EOF(End of File,为一个-1的常量):

#include <stdio.h>

int main() {
    char ch;

    printf("Enter your article (press Ctrl+D or Ctrl+Z to end):\n");

    // 循环读取字符直到EOF
    while ((ch = getchar()) != EOF) {
        putchar(ch); // 将读取的字符输出到标准输出
    }

    return 0;
}

操作类函数(strcat、strcpy、strncpy、strcmp、strlen、strlwr、strupr)

参阅P158,用到再查,没必要一次性记那么多……

strlen:测出的长度不包括”\0“

杂七杂八的

还有啥没讲

  • 整数的原码、反码与补码表示
  • 从堆区申请空间,实现动态数组(要考)
  • 指针与多维数组
  • 全局变量与局部变量、变量存储位置
  • 指针与函数、函数指针的应用
  • void* 等
  • 二重指针、多重指针

建议怎么学

先学C:老老实实地把我说的这些原理学透就行,语法后面忘记了都是无所谓的,但是得记住原理。

然后上C++/Java:两个对比学习

最后你就发现Python、PHP、JavaScript不用学了

想要深究计算机原理:推荐去学习汇编语言

参考

  • 书籍名称:C语言程序设计(第五版)——谭浩强、算法竞赛入门经典(紫书)
  • 章节名称:第6章 利用数组处理批量数据

[整理] 浅谈堆、栈、堆区、栈区的概念和区别 – 哆啦梦乐园 – 博客园 (cnblogs.com)

浅析C语言中的内存模型我正在参加「掘金·启航计划」 前言 近来学习深觉自己在语言的底层方面理解欠缺, 正好之前有看过一点 – 掘金 (juejin.cn)

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇