Category Archives: 课程学习

【C语言笔试复习】指针数组和数组指针

这周开始做C语言笔试卷,什么嘛!考的都是概念,各种细节,虽然貌似书上都能找到,但我还真不清楚…顿时又有一种我C语言学的好渣的感觉 T_T

好了,说下在【上机实验蓝皮书背后,综合测试题二】中难到的2道题,这两道题使得你对【指针数组】和【数组指针】的区别更加清晰。

【例题1】

有以下程序:

#include 
 
int main()
{
    char *s[] = {"one", "two", "three"}, *p;
    p = s[1];
    printf("%c, %s\n",*(p+1), s[0]);
    return 0;
}

执行后的结果是_______。

A. n, two B. w, one C. t, one  D. o, two

第一次我选了 C,因为我以为 (p+1) 是指向 “three“ 的。这是对指针 p 类型的理解错误。

正确答案是 B,因为:

 #include 
 
 int main()
 {
     char *s[] = {"one", "two", "three"}; // s是一个指针数组,元素是3
                                          // 个指向字符串常量的指针
     char *p = s[1];  // p是一个指向字符串的指针变量
     printf("%c, %s\n",*(p+1), s[0]);
     // (p+1)是p的地址加一个字符内存的大小,从指向t变成指向w
     return 0;
}

所以,平时我们说的指向字符串的指针,其实都是指向一个字符,所以对它进行位移运算时,加减都是1。
另外,如果你把第8行

   printf("%c, %s\n",*(p+1), s[0]);

改成

   printf("%c, %s\n",*p, s[0]);

输出就会是:wo,one

因为平时我们输出字符串的时候,实际上都是把字符串的首地址传给 printf( ) 函数,它通过末尾的 ‘\n’,来判断是否结束。

【例题2】

#include 

int main()
{
    int a[3][4] = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}};
    int (*p)[4] = a;
    printf("%d\n", *(*(p+1)+3));
    return 0;
}

以上代码执行的结果是_______。

答案是 8

这道题初看我完全不理解…怎么*了还能*,(*p)[4]又是什么鬼…我做这份卷子前把【指针数组】和【数组指针】搞乱了=

是这样的:

1.(*p)[4]:声明p是一个指向(4个int元素的数组)的指针,因此 p+1 从指向 a[0],变成指向 a[1]

2.*(*(p+1)+3):为什么会有两个星?

*(p+1) => a[1][0](也就是5),第一星从a[1]变成a[1][0],这个过程虽然地址没有变,但指针的类型变了!原来指针+1是加4个 int,现在指针+1只加一个 int 了!!

*(p+1)+3 => a[1][3](也就是8),再取一个*就是从地址里读出8

还可以修改一下原来的代码弄清楚:

 #include 
 
 int main()
 {
     int a[3][4] = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}};
     int (*p)[4] = a;   
     printf("p: %p\n",p);
     printf("p+1: %p\n",p+1);
     printf("*(p+1): %p\n",*(p+1));
     printf("*(p+1)+3: %p\n",*(p+1)+3);
     printf("%d\n",*(*(p+1)+3));
     return 0;
}

输出的结果是:

p: 0x7fff9f4ba180
p+1: 0x7fff9f4ba190
*(p+1): 0x7fff9f4ba190
*(p+1)+3: 0x7fff9f4ba19c
8

好啦~

这篇笔记最早发在博客园里,直接粘贴过来再排版的,所以格式不太美观,可以看→原文

C指针学习吐槽

上周我一直在通过看书、实践来试图弄懂指针。

一直到学习指针之前,我几乎都是不看书的。也没有看老师上课的PPT,因为感觉老师的PPT经常有错误,上课用还凑合,但自己课下看实在是很不舒服。我手头上有两本教材,一本是科大自己的《计算机程序设计(C语言版)》(机械工业出版社的),另一本是C语言经典教材《C程序设计语言(第二版)》是 The C Programming Language 翻译过来的。

按说第二本显然更好。因为LUG里面众人的吐槽和推荐,我不是很相信学校的教材,你看全书代码风格都不统一。而第二本虽说好,之前在阮一峰的博文 C语言学习教材 上面看到,并不适合新手。= =

上周C指针的学习,一开始是看经典教材的,结果看到字符指针就看不懂了,主要是作者为了引入字符指针举了个非常麻烦的例子。几乎看不懂!!

我发现C语言教材有这样的问题,它假设你之前的部分都看过了,所以不时的引入之前的内容而且不附加说明。造成的结果,你即使按照教材上面的代码输了一遍也往往跑不起来。

反正,最后放弃 The C Programming Language 我也认为它太难,太生涩,对新手不友好!相反,上周最后发现,学校的这本教材对指针的解释还可以接受=。就决定看这个了,感觉C指针还是需要资料的指导的。

所以,不仅人和人之间的信任是很重要的,对书本的信任也是很重要的…

看地址学指针2:字符指针

字符指针,也就是指向字符数组(字符串常量)的指针,比起指向数组的指针有略微不同。似乎是因为字符串常量特有的性质。还是通过代码来说明好了。

字符数组

11261

11262

以上操作,并没有体现出字符数组和之前我们学过的数组有什么不同。我只是想看看字符数组的地址啦~(看地址强迫症)

指向字符数组的指针

先回顾一下指向数组的指针

112611

pa 是指向数组 a 的指针变量

112612

pgreeting 是指向字符数组 greeting 的指针变量

112613

所以,从输出来看,指向数组的指针变量和指向字符数组的指针变量没什么区别嘛!输出的地址都差不多长= = 其他的一些操作也应该一样。

字符指针的特殊之处

以上操作实在没搞懂字符指针和普通的指向数组的指针有什么区别…

但字符指针可以这么操作:

1126111

1126112

如果字符指针有特殊之处,就在于它特殊的赋值方式…… 也就是第9-11行,右边写的是字符串常量却代表的是字符数组的指针。

另外还有的就是,由于这样:这些字符指针变量是指向字符串常量的指针,由于常量和变量的储存位置不同,地址要短很多。而还有的就是,两个指向”hello world!”的地址是一样的。

这些应该都是C语言实现的问题。

printf() 函数和字符指针

课本上说,下图第5行的语句中,printf 接受的是一个指向字符数组第一个字符的指针。也就是字符串常量可通过一个指向其第一个元素的指针访问。

好,我们看下以上的代码,pchar 是指向字符串常量第一个元素的指针,它的地址是 0x400671,然后我们可以通过 %s,pchar 来输出这之后的字符,直到遇到 ‘\0’。

所以,以上第9行和第10行应该是等价的,而第5行应该是它们的缩写。感觉上9、10行的写法更规范一些。

看地址学指针1:指针和数组

数组:

学习数组的时候老师强调,数组是在内存空间上连续分布的一群相同数据类型的元素的集合。图中,数组并没有初始化,但运行时仍然分配了内存

11251

11252

的确是这样,并且数组名 a (它的值是地址)和数组第0个元素 a[0] 地址是一样的。

 

指向数组的指针:

11253

11254

以上代码和运行结果就说明了,申明一个指针 pa 指向数组 a 后:

  1. pa + i 和 &a[i] 是等价的
  2. *(pa + i) 和 a[i] 是等价的

事实上,即使你让数组越界也不会有什么事情发现,地址输出仍然是正确的,只是值是不可预知的。如下代码所示:

11255

11256

指向数组的指针没什么特别的。如下,我们可以用指针变量 pa 作为条件判断依据:

11257

11258

 

数组名不是指针变量

数组名是数组首个元素的地址,但不是指针变量。它和指针变量是有区别的!如下:

11267

11268

11265

11266

1.pa 是个变量,可以 pa++ 而 a++ 是非法的。

2.sizeof(数组名)得到的是数组的大小,而sizeof(指针)得到的就是指针的大小啦!

总的来说,数组名更像是指针,而不是指针变量

 

混淆:指针和指针变量

老师说过,指针是指针变量的值。似乎还说过指针和地址是等价的。我们说指针变量是有类型的(那么指针也有类型?),虽然我不是很清楚指针和地址的更多区别,但为了避免混淆,我们还是尽量明确“指针变量”,感觉平时说的“指针”,很多时候是指“指针变量”…这里只是提一下。

 

PS:接下来这部分内容主要参考<The C Programming Language>(C程序设计语言-第2版)来写

这篇文章对应于 第83页 5.3 指针与数组

第5.4 地址算术运算,这部分我没看懂….先跳过了

看地址学指针0:指针的引入

指针的定义

22222

指针的输出(查看地址)

如何printf一个指针呢?指针的格式控制字符是 %p

嗯,从输出结果知道:我的64位(bit)机器上地址大小是8字节(byte)(8 byte = 64 bit)

O(∩_∩)O哈!新技能get√!!以后一定要多多输出指针,当搞不清楚指针到底指向哪里的时候,不妨输出地址看看嘛!

读地址

我们看上图中 x 的地址是 0x 开头的一串字符,也就是十六进制数。地址在计算机中是独一无二的,没有重复的地址,地址的大小在 [0, 2^64 – 1]范围内

2进制64位数字用16进制表示应该有16位,因为每相邻4位数可以等价于1位16进制位。而x的地址之所以不足16位是因为:它把前面的0省略了。我们还可以看到,地址0是(nil),地址1是0x1。

运算符的优先级:* 和 i++ 是一样的

首先,常用运算符的优先级是这样的:

我们知道 i++ 和 ++i 是不一样的:

i++

which means that the current value is used in the expression, and then it is incremented

++i

the current value is incremented first, and then it is used in the expression.

举例:

现在我们看下 * 的优先级:

以上的代码就能说明 *y++ 的执行顺序是从右到左的

这部分代码说明的问题还是很好玩的,我们来解读一下:

  1. 你可以发现两次输出的地址(y的值)差4, 因为int大小位4个字节,也就是 y++ 表示 y = y + 4,加的其实是地址的值
    y = 0x7ffff3405b2c
    y = 0x7ffff3405b30
    0x7ffff3405b2c + 4 = 0x7ffff3405b30
  2. 由于 x 被初始化为1,故 *y = 1;但是 y++之后,y指向x地址的后一个地址,它并没有初始化,故 *y 将不知道得到什么(也就是出现无可预知的值)
  3. (*z)等价于 x,然后我们发现第一次 y 的值和 z 的值是相等的,
    z = 0x7ffff3405b2c
    也就是 x = x + 1,只是把 x 的值加1,并没有改变 x 的地址。相当于往 x 的内存地址上,把原来的值擦掉,写上新的值
  4. *运算,i++,++i 优先级是一样的

 

好啦,最后还有一个新的发现:可执行文件每执行一次,内存地址就重新分配一次!

另外刚刚尝试了这个

* 和 & 互为逆运算

编译时发生了什么?

一、你知道编译的时候发生了什么吗?

你知道C语言编译时发生了什么吗?

是不是像我一样嫌弃老师上课讲的冗长又抽象,搞什么嘛!

说那么多还不如把细节展示给我看!

这周的Shorts视频中有一集叫做Compilers,7分钟不仅详细讲了编译时的四个步骤,还打开查看了 -E -S -c之后的文件。看完感到极大的满足,于是动手试了一遍,下面把视频转换成图文,跟我一起学C语言啦!

首先,写一个大家非常熟悉的”hello world!”代码

哦,我用的编辑器是emacs,操作系统是ubuntu(linux),然后直接在终端(Terminal)下用gcc编译,然后 ./a.out 执行。运行正常

这就完成了一次编译到运行的操作。平时我做作业也都是这样,并没有理会从C语言到机器语言的编译过程具体是什么样。

二、编译的四个步骤

gcc的编译流程分为四个步骤,分别为:

・ 预处理(Pre-Processing)
・ 编译(Compiling)
・ 汇编(Assembling)
・ 链接(Linking)

(1)编译预处理(Pre-Processing)

先输入这一行命令:gcc -E 目标文件.c

回车后就被刷屏了….

把这些代码导入到一个hello2.c的文件中

打开后

这是什么鬼!长这么奇怪= =

哈,居然有800多行,拉到最后终于看到熟悉的 “hello world!”代码片段惹!

这就是编译预处理啦~ 搜了一下什么是编译预处理

【摘】预编译的主要作用如下:

●将源文件中以”include”格式包含的文件复制到编译的源文件中。
●用实际值替换用“#define”定义的字符串。
●根据“#if”后面的条件决定需要编译的代码。

是嘛~刚刚刷屏那个就是把源代码中 #include <stdio.h> 这一行替换成了834行代码..

再拿宏定义试试,注意第3行

相同操作之后,对比预处理之后的.c文件

喏,836行之前没有区别!只有原来 #define name “Jenny” 那一行消失了,取而代之的是源代码中 printf(“Hello %s\n”,name); 中的name 被换成了 ”Jenny”

这就是传说中的,define只做“文本替换”啦

(2)编译(Compiling)

下面用 gcc -S 目标文件.c 来编译,用C语言代码 生成 汇编语言

打开编译后的 hello.s 文件

又是什么鬼,完全看不懂….

(3)汇编(Assembling)

用 gcc -c 目标文件.s 把

打开 hello.o二进制文件

这就是传说中的机器码了,你看到中间的 “hello world!” 了吗?

*Bonus 直接修改二进制文件

让我们改改机器码试试!→_→

改成 no zuo no die ….

运行试试

玩坏了 ..>_<.. 一定是字节不一样!!

改成hello jenny

这样就行啦!

*Bonus 用十六进制看机器码!

哈哈哈,有点像传说中的10101010串了吧!我不知道怎么通过二进制查看机器码….

再改成nozuonodie!!

这次成功了

(4) 链接(Linking)

刚刚一直到这个步骤,其实代码都不能执行。可执行文件 a.out 是我之前一步gcc hello.c生成的。

最后一步链接,需要在二进制文件 hello.o 的基础上,生成可执行文件 .out

命令为 gcc hello.o

列出当前文件夹下面的文件,可以发现只有 hello.out 是绿色的,并且最左边一堆rwx里面只有hello.out带有x,那些rwx表示的是”read”,”write”,”execute”的权限啦!