目录
2. 在windows32/Visual C系统中, 计算end:
(1) C语言中,int *p[n]和int (*p)[n] 的区别:
4. 请说出下面函数的功能:如果str2是str1的子集,则返回str2在str1中的位置,否则返回空。
7. 有如下代码,请问 myArray[2][3] = ?,答案是 11 。
写在前面:
在C语言软件开发的笔试题中,涉及最多的还是:指针、字符串、数组三者的组合,以及链表。
1. 编写代码实现函数strcat:
char *strcat(a, b) 将字符串 b 链接到 a 的尾部,并返回 a 。
#include <stdio.h>
#include <string.h>
int main()
{
char a[30];
char b[10];
int len1, len2;
int i;
printf("请输入字符串a:\n");
gets(a);
printf("请输入字符串b:\n");
gets(b);
len1 = strlen(a);
len2 = strlen(b);
for(i=0; i<=len2; i++)
{
a[len1 + i] = b[i];
}
printf("%s\n", a);
}
在使用gcc编译运行的时候,出现个错误:
sunyuhang@666:~/c$ gcc 1.c -o 1
1.c: In function ‘main’:
1.c:11:5: warning: implicit declaration of function ‘gets’; did you mean ‘fgets’? [-Wimplicit-function-declaration]
11 | gets(a);
| ^~~~
| fgets
/usr/bin/ld: /tmp/ccSLvyzt.o: in function `main':
1.c:(.text+0x34): warning: the `gets' function is dangerous and should not be used.
网上有个解决方法是:
用gets的话不知道这个数组能写进多少个字符,并且无法判断要不要继续写入用户输入的字符,这样的话会导致程序出现不可预知的行为——崩溃、缓存未知数据之类的。
简单来说就是,把gets改成fgets,后面加上字符串长度以及标准输入,例如 fgets(array, 10, stdin);
于是顺便学习一下scanf,gets,fgets的用法:
gets() 函数的函数原型为: char* gets(char* buffer);
调用格式为: gets(s); //其中s为字符串变量(字符串数组名或字符串指针)
gets(s)函数与scanf("%s",s)相似,但不完全相同:
- 使用scanf("%s",s) 函数输入字符串时,如果输入了空格会认为字符串结束,空格后的字符将作为下一个输入项处理;
- gets()函数用来从标准输入设备(键盘)读取字符串直到换行符结束,但换行符会被丢弃,然后在末尾添加'\0'字符,作为C语言字符串结束的标志。
gets函数的缺点:
gets() 可以无限读取,不会判断上限。所以程序员应该确保buffer的空间足够大,以便在执行读操作时不发生溢出。也就是说,gets()函数无法检查缓冲区的空间。
因此,当用户在键盘上输入的字符个数大于缓冲区buffer的最大界限时,gets函数也不会对它进行任何检查,这样极其不安全。因此,可使用 fgets() 函数替换 get() 函数。
相对于 gets 函数,fgets() 函数最大的改进就是能够读取指定大小的数据,从而避免 gets 函数从 stdin 接收字符串而不检查它所复制的缓冲区空间大小导致的缓存溢出问题。当然,fgets 函数主要是为文件 I/O 而设计的(注意,不能用 fgets 函数读取二进制文件,因为 fgets 函数会把二进制文件当成文本文件来处理,这势必会产生乱码等不必要的麻烦)。其中,fgets 函数的原型如下:
char *fgets(char *buf, int bufsize, FILE *stream);
该函数的参数含义:
- bufsize:用来指示最大读入字符数;
- stream:说明读取哪个文件,如果是从键盘上读入数据,可使用 stdin 作为该参数。
与 gets 函数不同的是,如果 fgets 函数读到换行符,就会把它存储到字符串中,而不是像 gets 函数那样丢弃它。即给定参数 n,fgets 函数只能读取 n-1 个字符(包括换行符)。如果有一行超过 n-1 个字符,那么 fgets 函数将返回一个不完整的行(只读取该行的前 n-1 个字符)。但是,缓冲区总是以 null('\0') 字符结尾,对 fgets 函数的下一次调用会继续读取该行。
也就是说,每次调用时,fgets 函数都会把缓冲区的最后一个字符设为 null('\0'),这意味着最后一个字符不能用来存放需要的数据。所以如果某一行含有 size 个字符(包括换行符),要想把这行读入缓冲区,要把参数 n 设为 size+1,即多留一个位置存储 null('\0')。
2. 在windows32/Visual C系统中, 计算end:
end = 0x8040 0000
unsigned int *start = 0x80000000;
unsigned int *end = start + 0x100000;
在C语言中,当你对一个指针进行加法运算时,实际上增加的是它所指向的数据类型的大小乘以你加的数。在这个例子中,start是一个指向unsigned int的指针,假设unsigned int的大小为4字节,那么start + 0x100000实际上是将start的地址增加了0x100000 * 4字节。
3. int *p[n]和int (*p)[n]
若有以下定义和语句:
int w[2][3], (*pw)[3];
pw = w;
则对数组元素的合法引用是: ACD
A. *(w[0] + 2) B. *(pw+1)[2] C. pw[0][0] D. *(pw[1] + 2)
解答:
(1) C语言中,int *p[n]和int (*p)[n] 的区别:
先来分析字符优先级: [ ] 的优先级大于 * ,() 和 [ ] 具有相同的优先级。
- int *p[n] 就等价于int *(p[n]) = (int *)(p[n])。可以看出来,这是个指针数组:是一个数据类型为 int *,以n个指针(指向地址)为元素,数组名为p的数组。
- int (*p)[n] 可以写成 (int) ( (*p)[n] ),是一个n维数组,数据类型为 int 。但是这个数组的首地址是*p,也就是说,p指向的内容,是一个数组的首地址。那么,p就是指向一个数组的指针,这个数组中的元素都是int。实际上,p相当于是一个二维指针。
知识点:
在C中,假设一个数组a[ ],数组名a是个常量指针,指向数组的起始地址,也就是数组第一个元素的地址,也就是 &a[0] 。它的类型取决于数组元素的类型。eg:int a[ ],数组元素类型为 int,则数组名就是“指向一个int的常量指针”。
举个例子代码如下(WIN32环境下):
#include <stdio.h>
int main()
{
int a[3] = { 1, 2, 3 };
printf("a = %p\n", a);
printf("&a[0] = %p\n", &a[0]);
printf("&a = %p\n", &a);
printf("a+1 = %p\n", a+1);
printf("&a[0]+1 = %p\n", &a[0]+1);
printf("&a+1 = %p\n", &a+1);
return 0;
}
运行结果为:
a = 00DEF97C
&a[0] = 00DEF97C
&a = 00DEF97C
a+1 = 00DEF980
&a[0]+1 = 00DEF980
&a+1 = 00DEF988
a 指向地址 0x00DEF97C,a+1 指向 0x00DEF980,步进了1个int型数据的地址空间(32位中int的长度是4)。a+1的地址相对于a的地址偏移了4个字节,很显然因为a是int型的指针。
&a 指向地址 0x00DEF97C,&a+1 指向 0x00DEF988,步进了1个int a[3]数组的地址空间(int a[3]的长度是12)。&a+1的地址相对于&a的地址偏移了12个字节,这正好是int a[3]数组所占内存空间的大小。所以 &a 是一个数组指针,指向具有3个元素的数组,而 &a+1 就表示跨过了该数组。
指针能表示两个维度的信息,第一个是内存当中的地址,第二个是访问内存的“尺度”。
数组名 a、数组首元素的地址 &a[0]、数组名取地址 &a,这三者在内存中其实是同一个地址,但访问内存的尺度有所不同,其中 a 和 &a[0] 是以 int 类型所占内存空间为尺度来访问内存,而 &a 是以数组 int a[3] 所占内存空间为尺度来访问内存。
原文链接:C语言数组名、数组名取地址、数组首元素地址之间的关系
(2)C语言一维数组的定义和引用:
一维数组元素的定义:
方括号中常量表达式表示数组元素的个数,又称数组的长度。eg:
int a[5],表示定义了一个有5个元素的整型数组a。
一维数组元素的表示:
数组元素的表示形式为:数组名[下标]
数组元素的编号又叫下标,表示了元素在数组中的顺序号,从0开始计算。
因此5个元素分别表示为a[0],a[1],a[2],a[3],a[4]。
注意,不能在方括号中用变量表示元素的个数,但是可以用符号常数或常量表达式,只能是整型常量或整型表达式。如为小数时,C编译将自动取整。
注意:
定义数组时用到的“ 数组名[常量表达式] ”和引用数组元素时用到的“ 数组名[下标] ”是有区别的:
- 定义数组时的常量表达式表示的是数组的长度;
- 而引用数组元素时的下标表示的是元素的编号。
看下面的程序:
#include <stdio.h>
int main(void)
{
int a[5];
a[5] = {1,2,3,4,5};
return 0;
}
错误:
① 下边一行的 a[5] 不是数组,而是数组中的一个元素,一个变量。
- 只有在定义的时候“a[常量]”表示的才是数组,此时方括号中的数字才表示数组长度。除此之外程序中任何地方看到“a[常量]”都不是数组,都只是数组的一个元素、一个变量,此时的“常量”表示的是元素的下标。
② 另外对于上面这个程序,数组 a 的元素只有 a[0]~a[4],并没有 a[5] 这个元素。
(3)二维数组:
eg:int a[3][4],定义了一个3行4列的二维数组,数组名为a,数组元素为int型。a[2][1]表示第二行第一个元素。
在C语言中,二维数组是按行排列的。
对二维数组的初始化,可以只对部分元素赋值,未赋值的元素自动取0
eg:int a[3][3] = {{1}, {2}, {3}}; 是对每一行的第一列元素赋值,未赋值的元素的值为0。
(4)字符数组:
用来存放字符的数组称为字符数组,也就是字符串。C语言用一个字符数组来存放一个字符串。
char str[30] = "www.abcdefg.com.cn";
将字符串直接赋值给字符数组,也可以不指定数组长度(不写30)。
数组第0个元素为 'w' ,以此类推。
字符串结束标志:
C语言中字符串以 '\0' 作为结尾。由 " " 包围的字符串会自动在末尾添加 '\0'。eg:"abc" 表面看只包含了3个字符,然而最后还有一个隐式的 '\0'。
需要注意的是,逐个字符地给数组赋值并不会自动添加'\0'
,eg:
char str[] = {'a', 'b', 'c'};
数组 str 的长度为 3,而不是 4,因为最后没有 '\0' 。
当用字符数组存储字符串时,要为 '\0' 留个位置。这意味着,字符数组的长度至少要比字符串的长度大1。请看下面的例子:
char str[7] = "abc123";
"abc123" 虽然只包含了6个字符,我们却将 str 的长度定义为7,就是为了能够容纳最后的 '\0' 。
也就是说,虽然系统会自动在字符串的结尾加 '\0',但它不会自动为 '\0' 开辟内存空间。所以在定义数组长度的时候一定要考虑 '\0'。
如果字符串的长度小于数组的长度,则只将字符串中的字符赋给数组中前面的元素,剩下的内存空间系统会自动用 \0' 填充。
字符串长度:
就是字符串包含了多少个字符(不包括最后的结束符 '\0')。例如, "abc"的长度是3。
C语言中用string.h头文件中的 strlen() 函数来求字符串的长度。eg:
#include <stdio.h>
#include <string.h>
int main(){
char str[] = "www.baidu.com";
long len = strlen(str);
printf("The lenth of the string is %ld.\n", len);
return 0;
}
输出为:
The lenth of the string is 13.
(5)字符串指针
① 使用指针的方式输出字符串:
#include <stdio.h>
#include <string.h>
int main(){
char str[] = "www.baidu.com";
char *p = str;
int len = strlen(str), i;
//使用*(p+i)
for(i=0; i<len; i++){
printf("%c", *(p+i)); //方法一
printf("%c", p[i]); //方法二
printf("%c", *(str+i)); //方法三
printf("%c", str[i]);
}
printf("\n");
}
方法一:使用 *(p+i) 输出字符串;
方法二:使用 p[i] 输出字符串;
方法三:使用 *(str+i) 输出字符串。
- 字符串中的所有字符在内存中都是连续排列的。
- 字符串中每个字符的类型都是 char,所以 str 的类型也必须是 char。
字符数组和字符串常量的区别
C语言有两种表示字符串的方法,一种是字符数组,另一种是字符串常量,它们在内存中的存储位置不同:
- 字符数组存储在全局数据区或栈区,有读取和写入的权限;
- 字符串常量存储在常量区,只有读取权限,没有写入权限。
看下面的代码:
#include <stdio.h>
int main(){
char *str = "Hello World!!!";
str = "I love C!!!"; // 1
str[3] = 'P'; // 2
return 0;
}
①是正确的,可以更改指针变量本身的指向;②是错误的,不能修改字符串中的字符。
获取用户输入的字符串就是一个典型的写入操作,只能使用字符数组,不能使用字符串常量。
(6)指针变量的运算:
对于指向普通变量的指针,一般不进行加减运算,虽然编译器不会报错,但这样做没有意义,因为不知道它后面指向的是什么数据。
数组中的所有元素在内存中是连续排列的,如果一个指针指向了数组中的某个元素,那么加1就表示指向下一个元素,这样指针的加减运算就具有了现实意义。
下面的代码演示了如何以指针的方式遍历数组元素:
#include <stdio.h>
int main()
{
int arr[] = {1, 2, 3, 4, 5};
int len = sizeof(arr) / sizeof(int); //求数组长度,整个数组所占用的字节数除以一个数组元素占用的字节数
for(int i=0; i<len; i++){
printf("%d ", *(arr+i) ); // *(arr+i)等价于arr[i]
}
printf("\n");
return 0;
}
其中,*(arr+i) 这个表达式:
arr是数组名,指向数组的第0个元素,表示数组的首地址,arr+i 指向数组的第 i 个元素,*(arr+i) 表示取第 i 个元素的数据,等价于 arr[i] 。
我们也可以定义一个指向数组的指针: int *p = arr; arr本身就是一个指针,可以直接赋值给指针变量 p 。arr 是数组第0个元素的地址,所以 int *p = arr 也可以写作 int *p = &arr[0];。
也就是说,arr、p、&arr[0] 三种写法是等价的,都指向数组的第0个元素。
还是上面的代码,如果我们要让 p 指向数组中的第二个元素:
int *p = &arr[2]; 也可以写做 int *p = arr + 2;
关于数组指针的谜题: *p++、*++p、(*p)++
① *p++:等价于*(p++) 。表示先取得第n个元素的值,再将 p 指向下一个元素。
注意:在上面的代码例子中,可以使用 *p++ 但是不能使用*arr++。因为 arr 是常量,而arr++ 会改变它的值。
② *++p:等价于*(++p)。表示先进行 ++p 运算,使得 p 的值加1,指向下一个元素,整体上相当于 *(p+1),所以会获得下一个数组元素的值。
③ (*p)++:表示先取第 n 个元素的值,再对该元素的值加 1。假设 p 指向第 0 个元素,并且第 0 个元素的值为 99,执行完该语句后,第 0 个元素的值就会变为 100。
(7)二维数组指针 (重、难点)
二维数组在概念上是有行有列的。但是在内存中,所有数组元素的分布是一维线性的,整个数组占用一块连续的内存。
二维数组是按行排列的,比如:int a[3][4],可以分解成三个一维数组,即 a[0]、a[1]、a[2],每一个一维数组又包含4个元素,例如 a[0] 包含 a[0][0]、a[0][1]、a[0][2]、a[0][3]。
先来定义一个指向 a 的指针变量 p: int (*p)[4] = a; 其中,* 表示p是一个指针,指向一个数组,数组的类型为 int[4],这正是 a 所包含的每个一维数组的类型。
对指针进行加减运算时,它前进/后退的步长与它指向的数据类型有关。
p 指向的数据类型是 int[4],那么 p+1 就前进4×4=16个字节,p-1就后退16个字节,正好是数组a所包含的每个一维数组的长度。也可以说成,p+1
会使得指针指向二维数组的下一行,p-1
会使得指针指向数组的上一行。
数组名 a 在表达式中也会被转换为和 p 等价的指针。
下面来探索一下如何使用指针 p 来访问二维数组中的每个元素。
① p 指向数组 a 的开头,也即第0行; p+1 前进一行,指向第1行。
② *(p+1): 表示取地址上的数据,也就是整个第1行的数据。
③ *(p+1)+1:表示第1行第1个元素的地址。
*(p+1) 单独使用时表示第1行数据,放在表达式中会被转换为第1行数据的首地址,也就是第1行第0个元素的地址。因为使用整行数据没有实际的含义,编译器遇到这种情况都会转换为指向该行第 0 个元素的指针。
就像一维数组的名字,在定义时或者和 sizeof、& 一起用时才表示整个数组,出现在表达式中就会被转换为指向数组第0个元素的指针。
④ *(*(p+1)+1):表示第1行第1个元素的值。
根据上面的结论,得出:
int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
int (*p)[4] = a;
a+i == p+i
a[i] == p[i] == *(a+i) == *(p+i)
a[i][j] == p[i][j] == *(a[i]+j) == *(p[i]+j) == *(*(a+i)+j) == *(*(p+i)+j)
指针数组和二维数组指针的区别:
在定义时非常相似,只是括号的位置不同:
int *(p[4]) = a; //指针数组,可以去掉括号直接写作 int *p[4]。
int (*p)[4] = a; //二维数组指针,不能去掉括号。
4. 请说出下面函数的功能:如果str2是str1的子集,则返回str2在str1中的位置,否则返回空。
char * abc(const char * str1, const char * str2)
{
char * cp = (char *)str1;
char *s1, *s2;
if(!*str2)
return((char*)str1);
while(*cp)
{
s1 = cp;
s2 = (char*)str2;
while(*s1 && *s2 && !(*s1 - *s2))
s1++, s2++;
if(!*s2)
return(cp);
cp++;
}
return(NULL);
}
5. 下列程序运行的结果是: om
#include<stdio.h>
#include<string.h>
char *ss(char *s)
{
return s + strlen(s)/3*3;
}
int main()
{
char *p, str[] = "www.h3c.com";
p = ss(str);
printf("%s\n", p);
}
6. 下列程序运行的结果是: ga
#include <stdio.h>
int main()
{
char a[] = "programming", b[] = "language";
char *p1, *p2;
int i;
p1 = a;
p2 = b;
for(i=0; i<7; i++)
{
if(*(p1+i) == *(p2+i))
{
printf("%c", *(p1+i));
}
}
}
7. 有如下代码,请问 myArray[2][3] = ?,答案是 11 。
int i, j;
int ctr = 0;
int myArray[4][4];
for(i=0; i<4; i++)
{
for(j=0; j<4; j++)
{
myArray[i][j] = ctr;
++ctr;
}
}