《C语言进阶剖析》6.数组指针与字符串(一)

一.《第26课 - 指针的本质分析》

 

1. 变量回顾

程序中的变量只是一段存储空间的别名,那么是不是必须通过这个别名才能使用这段内存空间?

2. * 号的意义

(1)在指针声明时,*号表示所声明的变量为指针

(2)在指针使用时,*号表示取指针所指向的内存空间中的值

3. 传值调用与传址调用

(1)指针是变量,因此可以声明指针参数

(2)当一个函数体内部需要改变实参的值,则需要使用指针参数

(3)函数调用时实参值将复制到形参

(4)指针适用于复杂数据类型作为参数的函数中

4. 常量与指针

(1)


二.《第27课 - 数组的本质分析》

 

1. 数组的概念

数组是相同类型的变量的有序集合

下图是一个包含5个int类型数据的数组的示意图:

 

2. 数组的大小

(1)数组在一片连续的内存中间存储元素

(2)数组元素的个数可以显示的或隐式的指定

观察下面两个数组,思考两个问题: a[2],a[3],a[4] 的值是多少?   b包含了多少个元素?

    

【数组的初始化】

#include <stdio.h>

int main()
{
    int a[5] = {1, 2};  // 编译器将未初始化的元素初始化为0
    int b[] = {1, 2};   // 数组未指定大小 ==> 编译器根据数组初始化的情况计算其大小(占用的空间最小)

    printf("a[2] = %d\n", a[2]);    // 0
    printf("a[3] = %d\n", a[3]);    // 0
    printf("a[4] = %d\n", a[4]);    // 0

    // sizeof(数组名) ==> 计算数组的大小
    printf("sizeof(a) = %zu\n", sizeof(a)); // 20
    printf("sizeof(b) = %zu\n", sizeof(b)); // 8

    printf("count for a: %zu\n", sizeof(a)/sizeof(a[0]));   // 5
    printf("count for b: %zu\n", sizeof(b)/sizeof(b[0]));   // 2

    return 0;
}

3. 数组地址与数组名

(1)数组名代表数组首元素的地址

(2)数组的地址需要对数组名使用取地址符 & 才能得到

(3)数组首元素的地址值与数组的地址值相同但是数组首元素的地址与数组的地址是两个不同的概念。

        我们通常所说的地址包括两个方面的概念:起始地址 和 所占用内存空间的大小。数组名仅仅对应数组一个元素那么大的内存,而数组的地址对应整个数组所占用的内存空间。

【数组名和数组地址】

#include <stdio.h>

int main()
{
    int a[5] = {0};

    printf("a = %p\n", a);          // 0x7ffc7425dbb0
    printf("&a = %p\n", &a);        // 0x7ffc7425dbb0
    printf("&a[0] = %p\n", &a[0]);  // 0x7ffc7425dbb0

    return 0;
}

4. 数组名的盲点

(1)数组名可以看作一个指针常量;注意这里只是说"可以看作",并非是真正的指针常量

(2)数组名"指向"的是内存中数组首元素的起始位置

(3)数组名不包含数组的长度信息

(4)在表达式中数组名只能作为右值使用

(5)只有在下列场合中数组名不能看作常量指针

  • 数组名作为作为 sizeof 操作符的参数,表示整个数组的大小
  • 数组名作为 & 运算符的参数,表示整个数组的地址

【数组和指针并不相同】

#include <stdio.h>

int main(){

    int a[5] = {0};
    int b[2];
    int* p = NULL;

    p = a;

    printf("a = %p\n", a);    // 0x7ffe40054a70, 数组a首元素的地址
    printf("p = %p\n", p);    // 0x7ffe40054a70
    printf("&p = %p\n", &p);  // 指针p的地址, 0x7ffe40054a68

    printf("sizeof(a) = %zu\n", sizeof(a));   // 数组a的大小, 20
    printf("sizeof(p) = %zu\n", sizeof(p));   // 指针的大小为8(64位系统)

    printf("\n");

    p = b;

    printf("b = %p\n", b);    // 0x7ffe40054a60, 数组b首元素的地址
    printf("p = %p\n", p);    // 0x7ffe40054a60
    printf("&p = %p\n", &p);  // 指针p的地址, 0x7ffe40054a68

    printf("sizeof(b) = %zu\n", sizeof(b));  //数组的大小, 8
    printf("sizeof(p) = %zu\n", sizeof(p));  //指针的大小为8(64位系统)

    // a = b;  // compile error, 数组名不能作为左值
               // error: incompatible types when assigning to type ‘int[5]’ from type ‘int *’

    return 0;
}


三.《第28课 - 指针和数组分析(上)》

 

1. 数组的本质

(1)数组是一段连续的内存空间

(2)数组的大小为 sizeof(array_type) * array_size

(3)数组名可以看作指向数组第一个元素的指针常量

思考下面两个问题:

  ① a + 1的意义是什么?结果又是什么?

  ② 指针运算的意义是什么?结果又是什么?

【a+1的结果是什么?】

#include <stdio.h>

int main()
{
    int a[5] = {0};
    int *p = NULL;

    printf("a = 0x%lX\n", (unsigned long)a);    // a = 0x7FFF043D43A0
    printf("a + 1 = 0x%lX\n", (unsigned long)(a + 1));  // a + 1 = 0x7FFF043D43A4,a[1]的地址

    printf("p = 0x%lX\n", (unsigned long)p);    // p = 0x0
    printf("p + 1 = 0x%lX\n", (unsigned long)(p + 1));  // p + 1 = 0x4

    return 0;
}

2. 指针的运算

(1)指针是一种特殊的变量,与整数的运算规则为:

   

       当指针p指向一个同类型的数组元素时,p + 1将指向当前元素的下一个元素,p -1指向当前元素的上一个元素。

(2)指针之间只支持减法运算,参与减法运算的指针类型必须相同

        对于数组而言,只有当两个指针指向同一个数组中的元素时,指针相减才有意义,其意义为指针所指元素的下标差。当两个指针指向的元素不在同一个数组时,结果未定义。

3. 指针的比较 

(1)指针也可以进行关系运算(<   <=   >   >=   ==   !=

(2)参与关系运算的指针类型必须相同

(3)指针关系运算的前提是:必须指向同一个数组中的元素

【指针运算初探】

#include <stdio.h>

int main()
{
    char s1[]={'H','e','l','l','o'};
    int i=0;
    char s2[]={'W','o','r','l','d'};
    char* p0 = s1;
    char* p1= &s1[3];
    char* p2 = s2;
    int*  p = &i;

    printf("%ld\n", p0 - p1);     // -3
    // printf("%ld\n", p0 + p2);  // compile error,指针之间不允许加法运算
    printf("%ld\n", p0 - p2);     // 语法正确,但是p0和p2指向两个不同的数组,结果没有意义
    // printf("%ld\n", p0 - p);   // compile error,不同类型指针之间不能相减
    // printf("%ld\n", p0 * p2);  // compile error,指针之间不允许乘法运算
    // printf("%ld\n", p0 / p2);  // compile error,指针之间不允许除法运算

    return 0;
}

【指针运算的应用】

#include <stdio.h>

// 数组元素的个数
#define DIM(a)  (sizeof(a) / sizeof(*a))

int main()
{
    char s[]={'H','e','l','l','o'};
    char* pBegin = s;
    char* pEnd = s + DIM(s);   // 数组末尾的后一个地址
    char* p = NULL;

    printf("pBegin  = %p\n", pBegin);
    printf("pEnd  = %p\n", pEnd);

    printf("Size: %ld\n", pEnd - pBegin); // 5

    for (p = pBegin; p < pEnd; p++)  // Hello
    {
         printf("%c",*p);
    }

    printf("\n");

    return 0;
}


四.《第29课 - 指针和数组分析(下)》

 

1. 数组的访问方式

(1)访问数组元素有两种方式:以下标的形式访问数组中的元素 和 以指针的形式访问数组中的元素。

              

(2)下标形式 VS 指针形式

  • 指针以固定增量在数组中移动时,效率高于下标形式
  • 指针增量为1且硬件具有硬件增量模型时效率更高
  • 下标形式与指针形式的转换

             

※※ 注意:现代编译器的生成代码优化率已大大提高,在固定增量时,下标形式的效率已经和指针形式相当,但从可读性和代码维护的角度来看,下标形式更优。

【数组的访问方式】

#include <stdio.h>

int main()
{
    int a[5] = {0};
    int* p = a;
    int i = 0;

    for(i=0; i<5; i++)
    {
       p[i] = i + 1;
    }

    for(i=0; i<5; i++)
    {
       printf("a[%d] = %d\n", i, *(a+i));
    }

    for(i=0; i<5; i++)
    {
       i[a] = i + 10;   // 等价于a[i] = i + 10
    }

    for(i=0; i<5; i++)
    {
       printf("p[%d] = %d\n", i, p[i]);
    }

    return 0;
}

【数组和指针不同】

ext.c

int a[] = {1, 2, 3, 4, 5};

29-2.c

#include <stdio.h>

// extern int a[];
extern int *a;

int main()
{
    printf("&a = %p\n", &a);
    printf("a = %p\n", a);
    printf("a[0] = %d\n", a[0]);

    return 0;
}

// extern int a[];
// &a = 0x601040
// a = 0x601040
// a[0] = 1


// extern int *a;
// &a = 0x601040
// a = 0x200000001
// 段错误 (核心已转储)

2. a 和 &a 的区别

(1)a 为数组首元素的地址

(2)&a 为整个数组的地址

(3)a 和 &a 的区别在于指针运算,前者针对的是数组的元素,后者针对的是整个数组

【指针运算经典问题】

#include <stdio.h>

int main()
{
    int a[5] = {1, 2, 3, 4, 5};
    int* p1 = (int*)(&a + 1);       // 指向数组元素5的后一个位置
    int* p2 = (int*)((int)a + 1);   // 指向数组 (起始地址 + 1字节) 处,这里是整数运算,不是指针运算
    int* p3 = (int*)(a + 1);        // 指向第2个元素

    printf("%d\n", p1[-1]);   // 5
    printf("%d\n", p2[0]);    // 33554432
    printf("%d\n", p3[1]);    // 2

    return 0;
}

3. 数组参数

(1)数组作为函数参数时,编译器将其编译成对应的指针    // 数组长度信息没有意义,都是退化为指针

(2)一般情况下,当定义的函数中有数组参数时,需要定义另一个参数来标示数组的大小

【虚幻的数组参数】

#include <stdio.h>

void func1(char a[5]) // char a[5] ==> char *a
{
    printf("In func1:sizeof(a) = %d\n", sizeof(a)); // 8 ==> a退化为指针

    *a = 'a';

    a = NULL;  // a可以作为左值,证明不是数组
}

void func2(char b[]) // 数组长度有无没有关系,char b[] ==> char *b
{
    printf("In func2:sizeof(b) = %d\n", sizeof(b)); // 8 ==> b退化为指针

    *b = 'b';

    b = NULL;  // a可以作为左值,证明不是数组
}

int main(){

    char array[10] = {0};

    func1(array);
    printf("array[0] = %c\n", array[0]); // array[0] = a;

    func2(array);
    printf("array[0] = %c\n", array[0]); // array[0] = b;

    return 0;
}


五.《第30课 - C语言中的字符串》

 

1. 字符串的概念

(1)字符串是有序字符的集合

(2)字符串是程序中的基本元素之一

(3)C语言中没有字符串的概念

  • C语言中通过特殊的字符数组模拟字符串
  • C语言中的字符串是以 '\0' 结尾的字符数组       // 回忆前面学过的转义符 \ ,'\0' 即八进制的0表示的字符,八进制的0在内存中就是0

2. 字符数组与字符串

(1)在C语言中,双引号引用的单个或多个字符是一种特殊的字面量

  • 存储于程序的全局只读存储区
  • 本质为字符数组,编译器自动在结尾加上 '\0' 字符

【字符数组与字符串】

#include <stdio.h>

int main()
{
    char ca[] = {'H','e','l','l','o'};
    char sa[] = {'W','o','r','l','d','\0'};

    char ss[] = "Hello world!";  // 使用"Hello World!"这个字符数组去初始化ss数组,这种语法是允许的!!!

    char* str = "Hello World!";  // 全局只读存储区,str[0]='h'是非法的

    printf("%s\n", ca);   // 打印Hello及其后面的内容,直到遇到 '\0'
    printf("%s\n", sa);   // World
    printf("%s\n", ss);   // Hello World!
    printf("%s\n",str);   // Hello World!

    return 0;
}

3. 字符串字面量

(1)字符串字面量的本质是一个字符数组

(2)字符串字面量可以看作指针常量

(3)字符串字面量存储在程序的全局只读存储区,不可对其中的字符进行修改

(4)字符串字面量至少包含一个字符,即空字符串 "" 包含一个 '\n'

【字符串字面量的本质】

#include <stdio.h>

int main()
{
    // 字符串字面量经过编译器编译,返回的是存储它的字符数组的地址,该字符数组村粗在程序的全局只读存储区
    char b = "abc"[0];
    char c = *("123" + 1);
    char t = *"";

    printf("%c\n", b);  // a
    printf("%c\n", c);  // 2
    printf("%d\n", t);  // 0

    printf("%s\n", "Hello");  // Hello
    printf("%p\n", "World");  // 0x400692,存储该字符串字面量的字符数组的地址

    return 0;
}

4. 字符串的长度

(1)字符串的长度指的就是第一个 '\0' 字符前出现的字符的个数

      

(2)函数 strlen 用于返回字符串的长度

【strlen的使用】 

#include <stdio.h>
#include <string.h>

int main()
{
    char s[] = "Hello\0World";  // 以第一个出现的'\0'作为字符串的结束符

    int i = 0;
    for (i=0; i<sizeof(s)/sizeof(s[0]); i++) {
        printf("%c\n", s[i]);
    }

    printf("%zu\n", strlen(s));   // 5
    printf("%zu\n", strlen("123"));   // 3

    return 0;
}

 


六.《第31课 - 字符串典型问题分析》

 


七.《第32课 - 数组指针和指针数组分析》

 

1. 数组的类型

  C语言中的数组有自己特定的类型,数组的类型由 元素类型 和 数组大小 共同决定。比如 int array[5] 的类型就是 int[5]float farray[10] 的类型就是 float[10]

2. 定义数组类型

C语言中通过typedef为数组类型重命名:

  typedef type (name)[size];

使用这种方式重命名如下两个数组

  typedef int (AINT5)[5];     typedef float (AFLOAT10)[10];

那么就可以使用 AINT5 和 AFLOAT10 定义数组,AINT5 iArray;   AFLOAT10 fArray   // iArray、fArray是数组名

3. 数组指针

(1)数组指针,顾名思义就是一个指针,只不过该指针用于指向一个数组

(2)前面学习数组时说过,数组名是数组首元素的地址,并不是整个数组的地址,使用&作用于数组名可以得到数组的地址

(3)定义数组指针的三种方式

① 可以通过数组类型定义数组指针:ArrayType *pointer;

② 可以直接定义:type (*pointer)[n];     // pointer为数组指针变量名,type为数组元素的类型,n为数组的大小

③ 先使用typedef重命名数组指针类型typedef type (*POINTER)[n],然后使用该类型定义数组指针变量 POINTER pointer;

【数组类型和数组指针】

#include <stdio.h>

typedef int (AINT5)[5];
typedef float (AFLOAT10)[10];
typedef char (ACHAR9)[9];

int main()
{
    AINT5 a1;   // int a1[5]
    float fArray[10];
    AFLOAT10* pf = &fArray;
    ACHAR9 cArray;

    char(*pc)[9] = &cArray;
    // char(*pcw)[4] = cArray;   // 类型不匹配,数组名表示数组首元素的地址,对数组名&表示数组的地址
    char(*pcw)[4] = (char(*)[4])cArray;

    int i = 0;

    printf("%zu, %zu\n", sizeof(AINT5), sizeof(a1));  // 20 20

    for(i=0; i<10; i++)
    {
        (*pf)[i] = i;  // fArray[i] = i;
    }

    printf("pf = %p, pf + 1 = %p\n", pf, pf+1);   // float占4字节 pf = 0x7ffdb9e61e10, pf + 1 = 0x7ffdb9e61e38

    for(i=0; i<10; i++)
    {
        printf("%f\n",fArray[i]); // 打印 0 到 9
    }

    // 0x7ffdc9e322a0, 0x7ffdc9e322a9, 0x7ffdc9e322a0 ,0x7ffdc9e322a4
    printf("%p, %p, %p ,%p\n", &cArray, pc+1, pcw, pcw+1);   // pc + 1 ==> (unsigned long)pc + 1*sizeof(*pc) ==> (unsigned long)pc + 9
                                                             // 同理 pcw + 1 = (unsigned long)pcw + 4

    return 0;
}

4. 指针数组

(1)指针数组,顾名思义就是一个数组,该数组的元素类型为指针类型

(2)指针数组的定义:

      type *PArray[n]

【指针数组的应用】

#include <stdio.h>
#include <string.h>

// DIM是维度(dimension)的缩写,一维数组的维度就是数组元素的个数
#define DIM(a) (sizeof(a)/sizeof(*a))

//table指向一个指针数组,即每个元素为指针类型
int lookup_keyword(const char* key, const char* table[], const int size) // ① 数组作为形参退化为指针类型  ② 需要传入数组长度信息
{                                           // char *table[] 等价于 char **table,但是后者看起来更加直观,推荐使用这种写法
    int ret = -1;
    int i = 0;

    for(i=0; i<size; i++) {
        if(strcmp(key, table[i]) == 0) {
            ret = i;
            break;
        }
    }

    return ret;
}

int main()
{
    const char* keyword[] = {   // 这里就是一个字符指针数组
           "do",
           "for",
           "if",
           "register",
           "return",
           "switch",
           "while",
           "case",
           "static"
    };

    printf("%d\n", lookup_keyword("return", keyword, DIM(keyword))); // 4
    printf("%d\n", lookup_keyword("main", keyword, DIM(keyword)));   // -1

    return 0;
}

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值