ToB企服应用市场:ToB评测及商务社交产业平台

标题: 关于指针、数组、字符串的恩怨,这里有你想知道的一切 [打印本页]

作者: 丝    时间: 2023-4-4 14:11
标题: 关于指针、数组、字符串的恩怨,这里有你想知道的一切
关于指针、数组、字符串的恩怨,这里有你想知道的一切


目录

内存组成

堆区

堆区 (Heap):由程序员手动申请释放的内存空间。
若不用free()释放,容易造成内存泄露(即内存被浪费、耗尽)。


栈区

栈区 (Stack):由系统管理,存放函数参数与局部变量。函数完成执行,系统自行释放栈区内存。
静态存储区

静态存储区 (Static Storage Area):在编译阶段分配好内存空间并初始化。
其中全局区存放静态变量(static修饰的变量)、全局变量(具有全局作用域的变量);常量区存放常量(又称为字面量)。
常量可分为整数常量(如1000L)、浮点常量(如314158E-5L)、字符常量(如'A'、'\n')和字符串常量(如"Hello")
const关键字修饰的的变量无法修改,但存放的位置取决于变量本身是全局变量还是局部变量。当修饰的变量是全局变量,则放在全局区,否则依然在栈区分配。
static关键字修饰的变量存在全局区的静态变量区。
常变量宏定义的概念不同。
常变量存储在静态存储区,初始化后无法修改。
宏定义在预处理阶段就被替换。不存在与任何内存区域。
代码区

代码区 (Code Segment):存放程序体的二进制代码。
  1. /*示例代码*/
  2. int a = 0;          //静态全局变量区
  3. char *p1;           //编译器默认初始化为NULL,存在静态全局变量区
  4. void main()
  5. {
  6.     int b;                //栈
  7.     char s[] = "abc";     //栈
  8.     char *p1 = "123";     //"123"在字符串常量区,p1在栈区
  9.    
  10.     p2 = (char *)malloc(10); //堆区
  11.     strcpy(p2, "123");       //"123"放在字符串常量区
  12.    
  13.     const int d = 0;      //栈
  14.     static int c = 0;     //c在静态变量区,0为文字常量,在代码区
  15.     static const int d;   //静态常量区
  16.    
  17. }
复制代码
字符串定义 - 一维

char s[10] = "Hello"

内存:静态存储区上的字面量"Hello"被复制到栈区,数组在栈区上的存储方式为'H''e''l''l''o''\0',可以通过s修改。但这不会影响到静态存储区上的"Hello"。
定义与使用:
  1. #include <stdio.h>
  2. void f(char s[10]) {      //等价于char *s
  3.     printf("%s\n", s);
  4. }
  5. int main() {
  6.     char s[10] = "LeeHero";
  7.     s[3] = 'Z';
  8.     printf("%s\n", s);   //输出:LeeZero
  9.     printf("%s\n", s+1); //输出:eeZero
  10.     printf("%c\n", s[3]);//输出:Z
  11.     f(s); //数组名作为函数参数传递时,会退化成指向数组首元素的指针 !IMPORTANT
  12.     return 0;
  13. }
复制代码
格式控制符 %s 跟随一个地址,并当做是字符串第一个元素对应的地址.
从该首地址开始解析,直到 '\0' 结束。
在这里指的是 s[0] = 'H' 的地址。
char *s = "Hello"

// 等价于const char *s = "Hello"
内存:s是指向字面量"Hello"的指针,字面量在静态内存区,因此该字符串不可被修改。
定义与使用:
  1. #include <stdio.h>
  2. void f(char s[10]) {       //等价于char *s
  3.     printf("%s\n", s);
  4. }
  5. int main() {
  6.     char *s = "LeeHero";
  7.     //s[3] = 'Z';          //无法执行
  8.     printf("%s\n", s);     //输出:LeeHero
  9.     printf("%s\n", s+1);   //输出:eeHero
  10.     printf("%c\n", s[3]);  //输出:H
  11.     f(s);
  12.    
  13.     return 0;
  14. }
复制代码
字符串定义 - 二维

char s[10][10] = {"Hello","World"}

内存:静态存储区上的字面量"Hello","World"被拷贝在栈区,与一维定义方式同理,可以通过语法糖s[j]修改字符。
定义与使用:
  1. #include <stdio.h>
  2. void f(char (*s)[10]) {        //形参s是个指针,指向有10个元素的字符数组
  3.                                //把(*s)[10] 改成 s[][10] ,其他不变,最后效果相同
  4.     printf("%s\n", s[1]);      //输出:Zero
  5.     s[1][0] = 'H';             //通过语法糖s[i][j]修改字符
  6.     printf("%s\n", s[1]);      //输出:Hero
  7.     printf("%c\n", s[0][1]);   //输出:e
  8. }
  9. int main() {
  10.     char s[10][10] = {"Lee","Hero"};
  11.     //s[1] = "Hey";            //无法执行,这种赋值方式仅在初始化时可用
  12.     s[1][0] = 'Z';
  13.     printf("%s\n", s);         //输出:Lee
  14.     printf("%s\n", *s+1);      //输出:ee
  15.     printf("%s\n", s[0]+1);    //输出:ee
  16.    
  17.     printf("%c\n", *(s[0]+1)); //输出:e
  18.     printf("%c\n", s[0][1]);   //输出:e
  19.    
  20.     printf("%s\n", s+1);       //输出:Zero
  21.     printf("%s\n", s[1]);      //输出:Zero
  22.    
  23.     f(s);
  24.    
  25.     printf("%s\n", s[1]);      //输出:Hero 这意味着函数内部的修改不是局部生效的
  26.     return 0;
  27. }
复制代码
对于打印结果的一些解释:
· 对二维数组进行操作与输出

  • s 等价于&s[0],是指向[存储"Lee"的一维数组]的指针
  • s+1等价于&s[1],是指向[存储"Zero"的一维数组]的指针
  • *s+1等价于(*s)+1,s通过*解析首先得到[一维数组"Lee"]
    即指向[一维数组"Lee"的第一个元素'L'的地址]的指针s[0];
    对该指针+1,相当于s[0]+1,使得指针指向[一维数组"Lee"第二个元素'e'的地址]
    格式控制符%s将该元素看成字符串的首地址,因而打印出"ee"
· 二维数组传参
二维数组主要有两种传参方式(以下两种是函数声明的方式。声明函数后,都是使实参为数组名来调用函数:f(s);)

  • void f(char (*s)[10]) {} —— 一维数组指针作形参
    二维数组名实际上就是指向一维数组的指针。因此这里形参s是个指向行元素的指针,与二维数组名匹配。
  • void f(char s[][10]) {} —— 二维数组指针作形参
    对于这种方法,仅二维数组的数组列数可以省略,不可省略行数。f(char s[][])是错误的。
    也就是说,1.和2.方式中都需要正确指定行数。
  • f(char **s),f(char *s[])的方式声明函数虽然能编译输出,但编译器可能会出现以下警告信息:
    1. [Warning] passing argument 1 of 'f' from incompatible pointer type
    2. [Note] expected 'char **' but argument is of type 'char (*)[10]'
    复制代码
    P.S. 当然,如果一定要用二维指针作实参f(char **s),在传参的时候可以将s强制转化:f((char **)s),函数内部操作元素可以通过*((int *)a+i*10+j)的方式……但何必呢。
    如果一定要试试,这里也有个例子:
    1. #include <stdio.h>
    2.             
    3. void f(char **s) {                     //形参s是个二维指针
    4.     printf("%c\n", *((char *)s));      //输出:L
    5.     printf("%s\n", ((char *)s));       //输出:Lee
    6.     printf("%c\n", *((char *)s+10));   //输出:H
    7.     printf("%s\n", ((char *)s+10));    //输出:Hero
    8. }
    9.             
    10. int main() {
    11.     char s[10][10] = {"Lee","Hero"};
    12.     f((char **)s);                     //“我一定要把s看做二维指针去传参!”
    13.     return 0;
    14. }
    复制代码
char *s[10] = {"Hello", "World"}

内存:类比char *s = "Hello",这里s是一个指针数组,s[0]、s[1]是两个指针,分别指向字面量"Hello"、"World"。指向的内容可以访问,无法修改。
定义与使用:
  1. #include <stdio.h>
  2. void f(char **s) {
  3.     printf("%s\n", s[0]);        //输出:Lee
  4.     printf("%c\n", s[0][0]);     //输出:L
  5. }
  6. int main() {
  7.     char *s[10] = {"Lee","Hero"};
  8.     printf("%s\n", s[0]);        //输出:Lee(等价于*s)
  9.     printf("%c\n", s[0][0]);     //输出:L  (等价于*s[0])
  10.     f(s);
  11.     return 0;
  12. }
复制代码
解释:
数组名作为函数参数传递时,会退化成指向数组首元素的指针。
当把s作为参数传递给f()函数时,实际上是把指针数组的首地址传递给了f()函数。这样,f()函数中的s就是一个二级指针,它指向了指针数组的第一个元素,也就是第一个字符串的地址。
f()函数接受一个二级指针作为参数。由此,f()函数中的s[0]和s[0][0]与主函数中的s[0]和s[0][0]含义相同。
  1. #include <stdio.h>
  2. int main() {
  3.    
  4.         /* s[10][10]与*s[10]的对比 */
  5.    
  6.     char *s[10] = {"Lee","Hero"};
  7.     printf("%d %d\n", sizeof(s), &s);            //输出:80 6487488
  8.     printf("%s\n", s);                           //无输出!
  9.    
  10.     printf("%d %d\n", sizeof(s[0]), &s[0]);      //输出:8  6487488
  11.     printf("%s\n", s[0]);                        //输出:Lee(等价于*s)
  12.    
  13.     printf("%d %d\n", sizeof(s[0][0]), &s[0][0]);//输出:1  4210692
  14.     printf("%c\n\n", s[0][0]);                   //输出:L  (等价于*s[0])
  15.    
  16.     char t[10][10] = {"Lee","Hero"};
  17.     printf("%d %d\n", sizeof(t), &t);            //输出:100 6487376
  18.     printf("%s\n", t);                           //输出:Lee
  19.    
  20.     printf("%d %d\n", sizeof(t[0]), &t[0]);      //输出:10  6487376
  21.     printf("%s\n", t[0]);                        //输出:Lee(等价于*t)
  22.    
  23.         printf("%d %d\n", sizeof(t[0][0]), &t[0][0]);//输出:1   6487376
  24.     printf("%c\n", t[0][0]);                     //输出:L  (等价于*t[0])
  25.    
  26.     /* *s[10]内容无法修改 */
  27.     t[1][0] = 'Z';           //修改二维数组元素
  28.     printf("%s\n", t[1]);    //输出:Zero
  29.     s[1][0] = 'Z';           //程序运行到这里崩溃!
  30.     printf("%s\n", s[1]);    //无输出!
  31.    
  32.     return 0;
  33. }
复制代码
对二维数组结构的认识

关于二维数组

a[j] : 第 \(i\) 行第 \(j\) 列元素
a:一级指针常量,指第 \(i\) 行首元素地址,第 \(i\) 行本质为一维数组,a+j是第 \(i\) 行第 \(j\) 列元素的地址
a:数组指针常量,是二维数组的起始地址,第 \(0\) 行的起始地址。
<img alt="image-20230323214508306" loading="lazy">
二维数组中的指针等价关系

优先级:() \(>\) ++ \(>\) 指针运算符* \(>\) +
[table][tr]二级指针




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4