1.结构体嵌套时的sizeof运算
写出下面各结构体的sizeof计算结构:
struct s1 {
char a[4];
};
struct s2 {
s1 a;
char b;
};
结构体s1所占用的空间为4个字节。
结构体s2的第一个成员a占用4个空间,第二个成员b占用1个空间,而结构体s1类型中占用空间最大的类型为char类型,是1个字节,因此结构体s2的sizeof运算结果只要是1的整数倍即可。最终结构体s2的sizeof运算结果为5.
2.指针与数组的区别。
char str1[15] = “hello world”;
char str2[15] = “hello world”;
尽管数组里面的内容完全一样,但是str1和str2这两个数组名等价于各自数组首元素的地址,所以str1 != str2。
char *str2 = “hello world”;
cha * str3 = “hello world”;
通过指针指向了同一地址,因此str2 == str3表达式的结果为真。
3.指针常量与常量指针
指针常量:本质上是一个常量,指针用来说明常量的类型,表示该常量是一个指针类型的常量。在指针常量中,指针自身的值是一个常量,不可改变,始终指向同一个地址。指针常量的用法如下:
int a = 10, b = 20;
int * const p = &a;
*p = 30;
const是用来修饰p的,因此p是常量,内容是不能改变的。*p是可以改变的,修改后p指向的内容为30.
常量指针:本质上是一个指针,常量表示指针指向的内容,说明指针指向一个“常量”。在常量指针中,指针指向的内容是不可改变的,指针看起来就好像指向了一个常量。常量指针的用法如下:
int a = 10, b = 20;
const int *p = &a;
p = &b;
第二行const与 * p 相邻,说明const是用来修饰 * p 的,因此 * p的值是不可改变的,即p指向的内容不可以通过p来修改,也就是说无法通过给 * p赋值来修改p指向的内容。
4.指针数组与数组指针
指针数组:存放指针的数组。
指针数组本质上是一个数组,指针是数组中的内容,表示数组中的每个元素都是指针,因此指针数组就是存放指针的数组。下面通过一个例子来了解指针数组的用法:
int a = 10, b = 20;
int *p[2];
p[0] = &a;
p[1] = &b;
在指针数组的定义中,根据运算符优先级,[ ]的优先级高于*的优先级,因此变量p与[2]结合,表明p是一个数组,而数组的元素的类型在变量的左侧声明,因此数组p是int * 类型的数组。
数组指针:指向数组的指针。
数组指针本质上是一个指针,数组是指针指向的类型,表示指针指向一个数组,因此数组指针就是指向数组的指针,下面通过一个例子来了解数组指针的用法:
int a[3] = {1,2,3};
int (*p)[3];
p = &a;
上面的代码定义了一个数组指针p,通过将p与* 括起来,使变量p先与*结合起来,表明p是一个指针,然后将(*p)遮住,剩余部分就是指针指向的类型,也就是int[3]类型,综上所述,变量p是一个指针,指针指向一个长度为3的int类型的数组,因此p是一个数组指针。
5.简述指针数组与指向指针的指针的区别
写出指针数组str中四个指针元素的值。
int main() {
char * str[4] = {"welcome", "to", "new", "Beijing"};
char ** p = str + 1;
str[0] = (*p++) + 1;
str[1] = *(p + 1);
str[2] = p[1] + 3;
str[3] = p[0] + (str[2] - str[1]);
char ** p = str +1;
数组名str等价于数组首元素的地址,而str+1表示数组中第二个元素的地址&str[1],因此指针p指向数组str中的第二个元素str[1],而str[1]本身也是指针,指向字符串“to”的首字符t,因此指针p是一个指向指针的指针。*p指向字符串“to”的首字符t。此外 *p++ 中的 *p没有括号并且是后++,因此赋值语句相当于执行了str[0] = *p + 1后再执行p++,而 *p+1指向字符串“to”的第二个字符o,因此str[0]指向了字符o,p++后p指向指针数租str的第三个元素str[2]。
str[1] = * (p + 1);
str[2] = p[1] + 3;
此时,p指向str[2],p + 1指向str[3],也就是p + 1 = &str[3],解引用可得*(p+1)= str[3],因此str[1] = str[3]。由于str[3]指向字符串“Beijing”的首字母B,因此str[1]也指向同样的位置。
在分析str[2]时,首先应该明确p[1]等价于*(p+1),这是访问数组元素的两种方式。根据对str[1]的分析可知,*(p+1)指向字符串“Beijing”的首字符B,也就是p[1]指向字符串“Beijing”的首字符B,p[1]+3就指向字符串“Beijing”的第四个字符j,因此str[2]就指向字符串“Beijing”的第四个字符j。
str[3] = p[0] + (str[2] - str[1]);
表达式str[2]-str[1]表示两个指针之间元素的个数,str[2]指向字符串“Beijing”的第四个字符j,而str[1]指向字符串“Beijing”的首字符B,相差3个字符,因此str[2]-str[1]=3。此外,p[0]等价于*(p+0),也就是*p,而 *p=str[2],因此p[0]同样指向字符串“Beijing”的第4个字符j,而p[0]+3就指向字符串“Beijing”的第7个字符g.
6. 简述指针与引用的区别
- 指针是变量的地址,引用是变量的别名。
sizeof运算的运算结果不同,指针进行sizeof运算得到的是指针本身占用的空间,返回结果为4个字节;而引用进行sizeof运算得到的是原变量占用的空间,返回结果取决于原变量的数据类型。
自增++运算符的意义不同,指针进行自增运算是对指针本身自增,使指针指向下一个地址空间,指针指向的变量没有改变;引用进行自增运算是对原变量的自增,改变原变量的值。 - 指针可以不初始化,引用必须初始化。
- 指针本身可以被修改,引用本身不能被修改。
- 指针可以为NULL,引用不能为NULL。
- 指针可以定义为二重指针,引用不能定义二重引用。
- 指针需要解引用,引用直接使用。
7. 程序中的变量存储区
下面程序中的变量定义都涉及哪些存储区?
int a = 0;
int main() {
char ch = 'a';
static int c = 0;
char *p1 = "abc";
char *p2 = "abc";
char *p3 = &ch;
char *p4 = (char *) malloc(10);
程序在main函数外定义并初始化了全局变量a,在main函数内定义并初始化了静态变量c,它们都保存在全局及静态存储区。
字符变量ch以及四个指针变量p1、p2、p3、p4都是普通的局部变量,它们保存在栈存储区。虽然四个指针指向了其他地址空间,这些地址空间位于不同的数据区段,但是指针变量本身保存在栈存储区。
字符串常量“abc”保存在字符串常量区,指针p1和p2指向同一个字符串常量,所以两个指针变量的值是相同的,指向字符串常量区的同一个地址。
程序最后一行通过malloc在堆存储区中动态申请了一块空间,并返回这块空间的首地址,指针p4指向这个地址。
四个指针变量中,p1和p2指向常量存储区中的地址,p3指向栈存储区中的地址,p4指向堆存储区中的地址,当然还可以创建指向全局和静态存储区的指针变量,但是无论指针指向哪个存储区,指针变量本身都保存在栈存储区。
8.简述栈空间和堆空间的区别
- 栈空间用于存储函数参数和局部变量,所需空间有系统自动分配,回收也由系统管理,无需人工干预;堆空间用于存储动态分配的内存块,分配和释放空间均由程序员控制,有可能产生内存泄漏。
- 栈空间作为一个严格后进先出的数据结构,可用空间永远都是一块连续的区域;堆空间在不断分配和释放空间的过程中,可用空间链表频繁更新,造成可用空间逐渐碎片化,每块可用空间都很小。
- 栈空间默认大小只有几M的空间,生长方式是向下的,也就是向着内存地址减小的方向消耗空间;堆空间的理论大小只有几G的空间,生长方式是向上的,也就是随着内存地址增大的方向消耗空间。
- 栈空间有计算机底层的支持,压栈和出栈都有专门的指令,效率较高;堆空间通过函数动态获取空间,涉及可用空间链表的扫描和调增以及相邻可用空间的合并等操作,效率相对较低。
9.简述递归程序潜在的风险
比如二叉树遍历算法的递归实现:
void preOrder(BiTree *root) {
if(node != NULL) {
visit(root);
preOrder(root->lchild);
preOrder(root->rchild);
}
}
递归算法存在一个问题:当递归层数过深时,有可能产生栈溢出。在递归调用过程中,每一次递归调用都会保留现场,把当前的上下文压入函数栈,随着递归调用层数的深入,压入函数栈的内容会越来越多,直到函数栈的空间用尽,而递归程序仍然没有满足返回的条件,继续向更深的一层调用,就会发生栈溢出。
下面是二叉树遍历的循环实现。
void preOrder(BiTree *root) {
stact <BiTree *> s;
BiTree *p = root;
while(p != NULL || !s.empty()) {
while(p != NULL) {
visit(p);
s.push(p);
p = p -> lchild;
}
if(!s.empty()) {
p = s.top();
s.pop();
p = p ->rchild;
}
}
}
10.不使用C/C++库函数,编程实现strcmp的功能
完整的算法描述如下:
int mystrcmp(const char *str1, const char *str2) {
if(str1 == NULL || str2 == NULL) {
printf("The string is error!\n");
exit(0);
}
while(*str1 != '\0' && *str2 != '\0' && *str1 == *str2) {
str1++;
str2++;
}
if(*str1 != '\0' && *str2 == '\0') {
return 1;
}
else if(*str1 == '\0' && *str2 != '\0') {
return -1;
}
else if(*str1 > *str2) {
return 1;
}
else if(*str1 < *str2) {
return 1;
}
11.编程实现strcpy的功能
char *mystrcpy(char *str1, const char *str2) {
char *p = str1;
if(p == NULL || str2 == NULL) {
printf("The string is error!\n");
exit(0);
}
while(*str2 != '\0') {
*p = *str2;
p++;
str2++;
}
*p = '\0';
return str1;
}
12.编程实现函数strstr的功能
char * mystrstr(const char *str1, const char *str2) {
char *src, *sub;
if(str1 == NULL || str2 == NULL) {
printf("The string is error!\n");
exit(0);
}
while(*str1 != '\0') {
src = str1;
sub = str2;
do {
if(*sub == '\0') {
return str1;
}
}
while(*src ++ == *sub++);
str1++;
}
return NULL;
}
13.简述memcpy与strcpy的区别
memcpy函数是C语言中的内存拷贝函数,它只提供了一般的内存拷贝功能,而不是针对字符串的。memcpy函数的原型为:
void *memcpy(void * dest, const void *src, size_t count);
strcpy是专为字符串拷贝定义的函数,其函数原型为:
char *strcpy(char *str1, char *str2);
其作用是将指针str2指向的字符串拷贝到指针str1指向的连续内存空间中,在使用strcpy进行字符串拷贝时,不仅拷贝字符串的内容,还会拷贝字符串的结束标志‘\0’。使用strcpy进行字符串拷贝时不需要指定拷贝的长度,函数会自动查找结束符‘\0’。