从开始学C语言到现在,已经过去了近6个月,初阶C语言我已经学完并复习整理了很多遍了,但是每次整理的时候总会发现一些自己遗落的知识点,还会发现一些自己之前没有注意的易错点,所以我决定对这些知识点进行一个记录。
1、TDD测试驱动开发。(来源:百度百科)
测试驱动开发,英文全称Test-Driven Development,简称TDD,是一种不同于传统软件开发流程的新型的开发方法。它要求在编写某个功能的代码之前先编写测试代码,然后只编写使测试通过的功能代码,通过测试来推动整个开发的进行。这有助于编写简洁可用和高质量的代码,并加速开发过程。
测试驱动开发的基本思想就是在开发功能代码之前,先编写测试代码,然后只编写使测试通过的功能代码,从而以测试来驱动整个开发过程的进行。这有助于编写简洁可用和高质量的代码,有很高的灵活性和健壮性,能快速响应变化,并加速开发过程。
测试驱动开发的基本过程如下:
① 快速新增一个测试
② 运行所有的测试(有时候只需要运行一个或一部分),发现新增的测试不能通过
③ 做一些小小的改动,尽快地让测试程序可运行,为此可以在程序中使用一些不合情理的方法
④ 运行所有的测试,并且全部通过
⑤ 重构代码,以消除重复设计,优化设计结构
简单来说,就是不可运行/可运行/重构——这正是测试驱动开发的口号。
2、
#include<stdio.h>
void fun(int *a,int *b)
{ int *c;
c=a;
a=b;
b=c;
}
int main( )
{ int x=3,y=5,*p=&x,*q=&y;
fun(p,q);
printf("%d,%d",*p,*q);
return 0;
}
这其实也是一种传值调用。本质上可以理解为交换指针,它只是改变了两个指针的指向,并没有对指针进行解引用操作,并没有改变x和y的值。
3、printf函数的返回值是打印字符的个数。scanf是有返回值的,返回的是输入值的个数,如果读取失败,就返回EOF。
4、全局变量未初始化时,默认赋值为0,int a;a=0。静态变量定义时未初始化,也为0。动态局部变量,未初始化时为随机值。
5、静态库。(来源:百度百科)
静态库是指在我们的应用中,有一些公共代码是需要反复使用,就把这些代码编译为“库”文件;在链接步骤中,连接器将从库文件取得所需的代码,复制到生成的可执行文件中的这种库。
程序编译一般需经预处理、编译、汇编和链接几个步骤。静态库特点是可执行文件中包含了库代码的一份完整拷贝;缺点就是被多次使用就会有多份冗余拷贝。
静态库和动态库是两种共享程序代码的方式,它们的区别是:静态库在程序的链接阶段被复制到了程序中,和程序运行的时候没有关系;动态库在链接阶段没有被复制到程序中,而是程序在运行时由系统动态加载到内存中供程序调用。使用动态库的优点是系统只需载入一次动态库,不同的程序可以得到内存中相同的动态库的副本,因此节省了很多内存。
6、隐藏代码。当你写了一个程序,想要投入市场给客户使用或者将该程序卖给别的公司做商用,同时不想要将自己的源程序代码直接公开时使用的,它可以避免部分程序代码呈现出来,而仅可以使用。
7、C99引入了变长数组,int arr[n] ,变长数组不可初始化。VS平台不支持变长数组。(来源:百度百科)
C语言中,直到C99标准出现之前,声明数组时在方括号内只能使用整数常量表达式。而C99做了很大改进,允许数组的[ ]中的值是整形变量或是整形表达式。这就解释了下面的情况:
int n;
scanf ("%d", &n);
int array[n];
虽然n确实是需要运行时动态确定的变量,但是在C99中,以这种变量作为数组大小的形式已经是允许的了。这样的数组就被称之为“变长数组”。
注意:变长数组是指用整型变量或表达式声明或定义的数组,而不是说数组的长度会随时变化,变长数组在其生存期内的长度同样是固定的。
8、二维数组只有在初始化和传参形参的时候可以省略行,定义的时候不可以(因为不知道该开辟多大的空间)。所以,定义数组时要给出空间的大小,如果没有给,必须要给出初始化结果。
9、C语言本身没有输入输出函数。C语言只规定了C语言的语法。输入输出函数是标准库提供的。标准库也是依据C语言的语法写出的函数。早期C语言是没有库函数的。标准库的实现在不同的编译上是有差异的。C语言规定了库函数的功能,参数和返回类型。
10、*(unsigned char *)&a运算顺序
(1).先取a的地址
(2).将&a强制类型转化为unsigned char *类型,也就是指向a的地址
(3).*取出unsiged char *指针的值。
同理,*( (void(*)())0 )()将0强制类型转换为函数指针,解应用后调用该函数,无传参。其实,作为函数指针,不加*也可以。
同理,int (*arr[10])[5] 是一个数组,里面每个元素都是一个指针,分别指向5个整形数组。
11、int i+unsighed int j,int也是要算数转化为unsighed int。
12、死循环的程序,为了缓慢观察,可以加上Sleep(1000)(单位是毫秒),<window.h>.
13、对于arr[ ],arr与&arr的区别。
arr是数组首元素地址,&arr取出的是一整个数组。&arr+1跳过一个数组。
14、数组指针。int(*p)[10],p等于&arr,*p等于*&arr,即arr。
注意:二维数组名为第一行一维数组的数组名,传参数时,形参要写指针,须要写int(*p)[10]。二维数组传参,形参行可以省,列不可以。
而 函数名和&函数名是一样的。int(*p)( int int ),(*p)(3 5),直接调用函数,并传参。但是这*是个摆设,因为原来函数名本来就等于它的地址,等效代替。
15、a<<2,a的值不变。a++或者++a,a的值会改变。
sizeof(int)的括号不可省略,sizeof(变量)可以。
16、递归最大的问题——栈溢出。
17、右移操作符:
(1)、算数右移:右边丢弃,左边补原符号位。(多为算数)
(2)、逻辑右移:右边丢弃,左边补零;
左移没有算术左移,只有逻辑左移,即右边补零。
18、sizeof()里的表达式是不实际进行运算的!!!例如:
#include<stdio.h>
int main()
{
short a=0;
int b=10;
printf("%d ",sizeof(a=b+5));//2
printf("%d",a);//0
}
这里,sizeof计算的是a这个变量的类型所占的空间大小,仍为短整型,字节为2;( )内部的表达式是不计算的,只是看似把b+5值赋给了a,a实际上还是0。
sizeof是不管()里面的东西合不合法的,它不管值属性,只管类型属性,比如说( )里面是一个不存在的东西,仍然会计算类型大小。例如:
#include<stdio.h>
int main()
{
int arr[3]={0,1,2};
printf("%d ",sizeof(arr[3]));//4
return 0;
}
这里arr【3】其实是越界的,是不合法的,但是sizeof不管这些,只计算arr[ ]的类型大小,即int 的字节数。
19、算数操作符,移位操作符,位操作符,==,参与表达式运算,都会整形提升,而!c不会,即逻辑运算不是算数运算,不会提升。
其实我们讨论这个整形提升这块儿,通常不会去考虑逻辑运算符、关系运算符的。因为像我们比较大小,进行逻辑语的判断,它只是进行一个真假的判断,只是判断两个数据,判断两个数据的大小、关系,所以我们通常不去讨论它。只有那些数学运算这块,我们才更关注它的一个整形提升。而像逻辑运算和关系运算这种,我们更关心的是它的其他的一些隐式类型转换,就比如有符号跟无符号之间的那种比较,那么它是以无符号转成无符号之后,然后再进行比较的。以及follow的跟double这种,还有int跟double,它是先转成一个浮点型,然后再去进行比较,而这其中所涉及到的一些误差信息,是我们需要考虑的信息,它并不需要去考虑整型提升,所以不用把这个问题复杂化了。
20、(指针-指针)一般只存在于同一个数组中,等于中间相差的元素个数,小地址处-大地址处为负数。指针是可以比大小的。
(指针-指针)其实是可以用在任何同一类型的指针的,但是真实情况下不会有人用两个整型指针相减,那将得到两个地址值相减的结果,没有任何意义。
21、char在内存中以ASCLL码存储和表示,是整形的一种。
22、无符号数很容易导致死循环。(具体原因以后会专门有一篇博客进行讲解)
23、void指针类型可以接受所有类型的指针,它不可以直接解应用,也不可以+-整数,因为它不知道它可以一次访问几个字节。应该强制类型转换后,再解应用。
24、size_t是unsigned int。
25、INT_MAX。
INT_MAX是代表最大整数值的宏。 同样,INT_MIN表示最小整数值。
这些宏在头文件<limits.h>中定义。任何整数变量都必须位于INT_MIN和INT_MAX之间。在几乎所有计算机中,最大整数值为2 ^(31)– 1 = +2147483647。最小整数值为-(2 ^ 31)= -2147483648。
26、空指针和空类型指针的概念:就是:
(1)、int*p=NULL,它是一个空指针,然后void* a是空类型指针;
(2)、空类型指针不可以解引用,也不可以进行+-整数运算;
(3)、空指针NULL指的是地址为0的那块空间,不可以访问,也不可以解引用。
空指针的赋值:空指针一开始地址为0,是看做无地址,如果想要通过它进行操作是无法找到它的地址的,但是我们可以申请另一个地址,比如创建一个另一个指针,把这个指针赋值给它,它就拥有了自己的地址。从此,就可以对它进行正常操作了。所以把别的指针或者是&a直接赋值给它是可以的。例如:int*p=NULL这里是将p指向了0x0000,此时p不能操作,只有将其指向其他可访问的地址,才可以解引用操作。
27、常指针、指向常量的指针和指向常量的常指针:
(1)、常指针:指针值在经过初始化之后不允许修改的指针,例如:
int a=10,b=20;
int* const p=&a;
*p=20;//合法,等同于a=20;
p=&b;//非法,试图改变P的值,指向另一个变量;
数组名就是一个最常见的常指针,也称为指针常量。p只能读取而不能修改,因此定义的时候就必须初始化使其具有确切的地址值。
(2)、指向常量的指针:指向常量的指针所指向的内容不允许通过该指针修改,例如:
int a=10,b=20;
int const* p=&a;
*p=20;//非法,不能用此方式改变a的值;
p=&b;//合法,改变P的值,指向另一个变量P;
表示指针指向的内容不允许通过指针修改,但是指针本身是变量,可以改变。该指针常在函数调用中使用,确保了实参变量不会通过指针形参被修改。
(3)、指向常量的常指针:其指针本身以及指向的内容都不允许被修改。例如:
int a=10,b=20;
const int * const p=&a;
*p=20;//非法,不能用此方式改变a的值;
p=&b;//非法,不能改变P的值,指向另一个变量b;
定义时就必须初始化使指针具有确定的地址值。
28、(*p)++,*p++和*(p++)的区别:
应该理解为,由于后++优先级高于*,应该先p++,后取值,但因为是后++,所以先执行*p,然后等赋值完成以后,p再++。
(*p)++就是先对p解引用,赋值,然后再把p向后移动一个元素。
简单来说,就是后两者没有区别,然后(*p)++和后两者的结果一样。
int main (void)
{
int a[]={4,5,6,7,8};
int *p = a;
*p++=100;
printf(“%d %d\n” , *p,*(++p));
return 0;
}
程序完成后输出什么?
答案:5 6
29、如果变量a是一个数组,a++是否合法?
因为数组名a代表数组首元素的地址,它是一个指针型常量,它的值在程序运行期间是固定不变的。所以a++是无法实现的。
就比如8++,是不合法的。
既然a++不合法,那么*a++也是不合法的,因为先++。
30、gets、puts和fgets、fputs的区别:
gets和puts 函数是C语言提供的标准函数 使用时要包含头文件stdio.h。
puts函数符串时,遇到'\0'结束,并将‘\0'转换为'n',也就是能自动进行换行的处理。而用 gets 函数读取字符串时,字符串连同换行符(即回车符)会依次读入指针形式参数ps指向的字符数组(要有明确指向,指向一个已定义的字符数组),并将换行符'\n' 转为串结束符‘\0’。
fgets:
从流中获取字符串
从流中读取字符并将其作为 C 字符串存储到 str 中,直到读取 (num-1) 个字符或到达换行符或文件末尾,以先发生者为准。
换行符使 fgets 停止读取,但它被函数视为有效字符,并包含在复制到 str 的字符串中。
终止空字符会自动附加到复制到 str 的字符之后。
请注意,fgets 与 gets 完全不同:fgets 不仅接受流参数,还允许指定 str 的最大大小,并在字符串中包含任何结束换行符。
fputs:
将 str 指向流的 C 字符串写入流。
该函数从指定的地址 (str) 开始复制,直到到达终止空字符 ('0')。此终止空字符不会复制到流中。
请注意,fputs 与 puts 的不同之处不仅在于可以指定目标流,而且 fputs 不会写入其他字符,而 puts 会自动在末尾附加换行符。
31、函数getchar和函数putchar:
函数getchar于从键盘读入一个用户输入的字符,并将该字符返回给前面等待输入的变量。当函数getchar前面没有要输入的变量时,即语句getchar();表示系统从输入缓冲区提取一个字符,但不赋给任何变量,也就是说忽略返回值。
函数putchar的作用时将给定的参数以单个字符的形式输出到显示器屏幕的当前位置上,其参数可以是字符常量、变量或表达式,也可以是字符的ASCLL码整型值。
32、用const修饰的只读变量与用#define定义的符号常量的区别只在于:只读变量有数据类型,而符号常量则没有数据类型。编译器只会对只读变量进行类型检查,而对符号常量则只进行字符串替换,不进行类型检查。
字符串替换时非常容易产生意想不到的错误,因此,我们在编程时尽量使用const修饰的只读变量代替符号常量。
33、求余运算要求两个操作数都必须为整型,结果为整数除后得到的余数。另外,余数的符号与被除数相同,例如:6%(-4)=2;(-6)%(4)=-2.
34、运算符的优先级与结合性只是决定了表达式的语义,即如何划分分子表达式,而与表达式的计算次序无关。
35、位运算的运算对象必须是标准的char和int 数据类型,而不能是float、double、long、double等其他复杂的数据类型。
36、swich后面的表达式可以为整型、字符型或者枚举型,但不允许时实型;case后面必须为常量,且类型应该与swich中表达式的类型相同。
37、当型循环比直到型循环更安全、更可靠。
38、static类型的全局变量仅限本文本使用,非static类型的全部变量其他文件使用extern声明后也可以使用。
对于全局变量,程序刚运行时就在静态数据区为其分配内存空间,未指定初值的情况下会自动初始化为0,直到程序运行结束时所占的内存空间才被回收。
而静态局部变量在程序开始运行时在静态存储区分配了存储空间,并一直占用到程序结束。也就是说,静态局部变量与全局变量在内存的分配与释放、占用区域、生命期都是一样的,但其作用域仍仅限于本函数,因为它仍然是局部变量。静态局部变量在程序刚开始运行时就被初始化,未指定初值的情况下会自动初始化为0。每次进入函数时不再初始化,而是保留上次的值。
形参是临时变量,不可以定义为static型。
未经初始化的自动局部变量的值是不确定的。
总结一下:数组初始化了,每个元素为0;没有初始化,为随机值;
+static,每个元素默认为0;全局变量,默认为0;局部变量,默认为随机值;
malloc创建的动态空间里面的数据默认为随机值;calloc则默认为0。
39、extern关键字只是用来声明全局变量而不是定义变量。变量声明与变量定义的区别类似于函数的原型声明与函数的定义,声明变量时不需要为变量分配内存空间。
extern可以使后定义的全局变量的作用域扩展到它前面的位置,使的全局变量成为真正意义上的全局变量。
extern int count;
如上使外部变量声明语句,其作用包括:
1、如果变量的定义和访问在同一个文件中,要访问后定义的全局变量,则需要在前面的位置做外部变量声明;
2、如果程序由多个文件组成,某文件中需要访问另一个文件中定义的全局变量,则一定要通过此语句在需要访问的文件中作外部变量声明。
40、寄存器变量
程序运行时,CPU 需要与内存之间进行数据交换,因而有一定的时间开销。
若能将一些简单而频繁使用的自动局部变量放到 CPU 的寄存器(Register)中,使得访问变量的速度与指令执行的速度同步,则程序的性能将得到一定的提高。
寄存器是 CPU 内部的一种容量有限但速度极快的存储器。寄存器变量就是用寄存器存储的变量。由于寄存器的数量有限,因此可定义的寄存器变量的有效数目依赖于具体的机器。寄存寄存量所占的字节数不能太多,一般只有int、char、short、unsigned和指针类型的变量定义为寄存器变量。此外,对寄存器型变量也无法进行取址。
现在的编译器都具有自动优化程序的功能,将普通变量优化为寄存器变量,无需用 register来定义。因此,在现代编程中,用户一般不需要去关心 register 关键字的使用。
41、在实际编程中,我们要合理使用变量的4种存储类型关键字。一般建议少使用外部变量,以降低各函数之间的耦合度,保证各函数功能的相对独立性。静态局部变量虽然用于某些特定的程序中可以简化程序,但占用内存时间较长且程序可读性较差,除非必要,也应尽量少用。自动局变量是使用最为广泛的一类变量,求解问题时所用到的中间变量一般都定义为自动局部变量。
42、一个中文字符占连续两个字节,相当于是2个char类型的值,因此对5个中文字符统计出的其他字符数是10个。
43、
void Swap(int* px, int* py)
{
int* tmp;
tmp = px;
px = py;
py = tmp;
//直接访问,交换px、py的值
printf("*px=%d,*py=%d\n", *px, *py);
}
int main()
{
int x = 3; int y = 5;
Swap(&x, &y);
printf("x=%d,y=%d",x,y);
return 0;
}
上面这个代码并不会实现x和y的交换,我们可以通过一个图来解释一下:
打印*px和*py的值的时候会发现它们交换了,但是实际上,这只是指针变量的值的交换。
这样才可以:
void Swap(int* px, int* py)
{
int tmp;
tmp = *px;
*px = *py;
*py = tmp;
//直接访问,交换px、py的值
printf("*px=%d,*py=%d\n", *px, *py);
}
int main()
{
int x = 3; int y = 5;
Swap(&x, &y);
printf("x=%d,y=%d", x, y);
return 0;
}
44、字符数组,因为数组名是指针常量,不能被赋值,因此不可这样写:
char ch[20] = { "Hello word!" };
ch = "programming!";
45、比较字符串的大小一定要用strcmp函数,因为如果直接比较字符数组名,相当于两个字符串的首地址在进行比较,比的是两个字符串的起始地址的大小,没有实际意义。
46、全局变量是指定义在函数外部的变量,这里的函数包括main函数,即
int count;
int main()
{
int i;
int j;
}
其中的count变量就是一个全局变量,而i和j则是main函数体内的局部变量;
注意,全局变量的作用域是程序中从定义开始(即,若定义位置不处在程序最开头,那么这个全局变量的作用域也不是整个程序)到程序结束为止,但是要去掉其中同名局部变量的作用域的部分。
来看个(此i非彼i)代码段,求一求程序的运行结果:
int fun(int i)
{
return i * i;
}
int main()
{
int i = 0;
i = fun(i);
for (; i < 3; i++)
{
static int i = 1;
i += fun(i);
printf("%d,",i);
}
printf("%d\n",i);
return 0;
}
//2,6,42,3
[解析]
此题中有多个 i,但是作用域各不一样。因此在程序执行的每一处,要弄清楚是哪一个i在起作用。
主函数开始定义了一个自动局部变量 i,此i作为第一次 fun 函数调用的实参,并且函数调用结果0也赋值给此i。
接着该i作为 for 循的控制变量,但进入循环体之后,复合语句中定义了一个静态局部变量i。
因此,复合语句中所有的i均为静态局部变量,第一次执行循环体的时候初始化了静态局部变量 i,因此第一次执行 printf 时的输出结果是fun的返回值1加上i本身的1 得到结果 2。
接着用于循环控制的自动局部变量 i再次进入循环,然后又是静态局部变量i在起作用,用上一次得到的结果 2 作为实参调用函数得到结果 4返回后再和i本身相加,得到结果 6。
随后,用于循环控制的局部变量自增 1变成了 2 再次进入循环。
然后又是静态局部变量 i起作用,用上一次得到的结果 6 作为实参调用函数得到结果 36,返回后再和i本身相加,得到结果 42。
接着,用于循环控制的自动局部变量i自增 1变成了结果 3,循环控制条件不再满足,循环终止,最后输出当前 i 的值为 3。
此题在答题过程中建议画出不同变量的内存空间示意图,以免混淆同名变量的
47、多个源文件同时用到的全局整数变量,它的声明和定义都放在头文件中,这是错误的,如果多个源文件共用一个全局变量,一般通过extern在头文件.h中声明该变量,然后在源文件.cpp中定义,如果在头文件中定义的话,多个源文件同时引用该头文件,会造成重复定义的错误。当然,具体的也可以参考另一篇博客里面通讯录的实现,里面的函数的具体实现(定义)都是放在“contact.c”中的,“contact.h”只是声明了这些函数,最后“test.c”和“contact.c”都引用了“contact.h”。
48、getchar、putchar、getch、putch函数属于屏幕控制函数,getch和putch被包含在conio.h中。通常,从键盘输入字符采用getchar,每输入一个字符,会在屏幕上由回显;但在密码输入时,回显会使得密码变成明码,所以我们可以改用getch,它在屏幕上没有回显。
getchar():
用户输入的字符会被先存放在键盘缓冲区中,直到用户按回车为止。getchar函数的返回值是用户输入的第一个字符的ASCII码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。即后续的getchar调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完为后,才等待用户按键。
putchar():向终端输出一个字符。
getch():从控制台读取一个字符,会等待你按下任意键,再继续执行下面的语句。
putch(ch):在当前光标处向文本屏幕输出字符ch,然后光标自动右移一个字符位置。
49、当有int型变量x、y、z,语句"if(x>y) z=0;else z=1"和(C)等价?
A. z = ( x > y ) ? 1 : 0 ;----------反了
B. z = x > y ;----------反了
C. z = x < = y ;当x<=y时,z=1;当x>y时,z=0;
D. z = x < = y ? 0 : 1 ;---------->与C相反。
50、下面程序段的运行结果是什么?
int a, b = 0;
for (a = 0, a++ <= 2;);
b + = a;
printf("%d %d\n",a,b);//4 4
注意,这里其实有一个大陷阱!for后面紧跟着一个“;”,所以它执行的其实是空语句!
a的变化过程:0,1,2 ,3,4(这里是后置++,最后判断a++<=2时,a=3,不符合,但是a仍然++了);
b=0+4=4。
51、引用数组元素的时候,其下标可以是整型常量或整型表达式,但是下标一定不可以越界。a[1>2][!1]=a[0][0]。
52、以下程序有错:
int main()
{
int* p, i;
char* q, ch;
p = &i;
q = &ch;
*p = 40;
*p = *q;
//...
return 0;
}
*p,*q得到的是int和char类型的变量值,在C语言中,char可以转化为int进行赋值操作,所以*p=*q是可以的。在定义中q指向变量ch,*q有意义,只是ch没有赋值,是个随机值,用它给p指向的i赋值没有任何意义。
53、设有语句int a[2][3],下面哪一种表示不能表示元素a[i][j]?
A. * ( a [ i ] + j ) B. * ( * ( a + i ) + j ) C. * ( a + i * 3 + j ) D. * ( * a + i * 3+ j )
[解析]
下标访问和指针访问方法中存在*(a+i)=a[i]的关系,因此选项 A、B 是相同的表示方法,可以访问 a[i][j];
数组名a 是行地址,因此选项C 是从数组开始处移动i*3+j的位置,这远超出数组的范围,是错误的;
选项D,*a 变成列地址,移动i*3+j列,而每行有3列,因此相当于移动i行再移动j列,正好移动到a[i][j]所在的位置,间接访问可以得到元素 a[i][j]。
54、sizeof是一个运算符。
55、以下表达式的值与x 物概,其值恒为真的是:(B)
A、( x > 10 ) | | ( x < 5 ) B、0 < x < 5
C、( x > 10 ) && ( x < 5 ) D、x > 5 && x < 10
0<x,如果为真,那么就是1<5,最后结果为真;如果为假,那么就是0<5,最后结果还是真。