c语言面试知识总结

来源:互联网 发布:成都真那么好么知乎 编辑:程序博客网 时间:2024/05/19 10:09

1、 Const:

(1)const修饰的是一个只读变量

(2)节省空间,避免不必要的内存分配,提高效率

编译器通常不为普通const只读变量分配存储空间,而是将它们保存在符号表中,这使

得它成为一个编译期间的值,没有了存储与读内存的操作,使得它的效率也很高。

例如:

#define M 3 //宏常量

const int N=5; //此时并未将N放入内存中

......

int i=N; //此时为N分配内存,以后不再分配!

int I=M; //预处理期间进行宏替换,分配内存

int j=N; //没有内存分配

int J=M; //再进行宏替换,又一次分配内存!

const定义的只读变量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define

一样给出的是立即数,所以,const定义的只读变量在程序运行过程中只有一份拷贝(因为

它是全局的只读变量,存放在静态区),而#define定义的宏常量在内存中有若干个拷贝。

#define宏是在预处理阶段进行替换,而const修饰的只读变量是在编译的时候确定其值。

#define宏没有类型,而const修饰的只读变量具有特定的类型。

(3)修饰一般变量,数组,指针,函数参数,函数返回值

 

2、 Static

(1)    修饰全局变量,称为静态全局变量。由于全局变量本身存储在静态区,因此本身就是静态的,对全局变量使用静态是告诉编译器这个变量只能在本文件中被使用,不能被extern

(2)    修饰局部变量,称为静态局部变量。存储在静态区,即使函数下次调用也不改变其值。

(3)    修饰函数。表示这个函数的作用域仅限于本文件。

 

3、 如果一个函数没有显式地声明返回值,那返回值就是Int型的

在c语言中,如果一个函数没有显式地说明参数是void,那么是可以使用参数的,如下所示:

#include<stdio.h>

 

void test(){

         printf("ok\n");

}

 

int main(){

         //test(3);

         return0;

}

在c++中不可以


4、 按照ANSI(AmericanNational Standards Institute)标准,不能对void指针进行算法操作,

即下列操作都是不合法的:

void * pvoid;

pvoid++; //ANSI:错误

pvoid += 1; //ANSI:错误

ANSI标准之所以这样认定,是因为它坚持:进行算法操作的指针必须是确定知道其指

向数据类型大小的。也就是说必须知道内存目的地址的确切值。

例如:

int *pint;

pint++; //ANSI:正确

但是大名鼎鼎的GNU(GNU's Not Unix的递归缩写)则不这么认定,它指定void *的算法

操作与char *一致。因此下列语句在GNU编译器中皆正确:

pvoid++; //GNU:正确

pvoid += 1; //GNU:正确

在实际的程序设计中,为符合ANSI标准,并提高程序的可移植性,我们可以这样编写

实现同样功能的代码:

void * pvoid;

(char *)pvoid++; //ANSI:正确;GNU:正确

(char *)pvoid += 1; //ANSI:错误;GNU:正确

GNU和ANSI还有一些区别,总体而言,GNU较ANSI更“开放”,提供了对更多语法

的支持。但是我们在真实设计时,还是应该尽可能地符合ANSI标准。

【规则1-36】如果函数的参数可以是任意类型指针,那么应声明其参数为void *。

典型的如内存操作函数memcpy和memset的函数原型分别为:

void * memcpy(void *dest, const void *src,size_t len);

void * memset ( void * buffer, int c,size_t num );

这样,任何类型的指针都可以传入memcpy和memset中,这也真实地体现了内存操作

函数的意义,因为它操作的对象仅仅是一片内存,而不论这片内存是什么类型。如果memcpy

和memset的参数类型不是void *,而是char *,那才叫真的奇怪了!这样的memcpy和memset

明显不是一个“纯粹的,脱离低级趣味的”函数!

下面的代码执行正确:

例子:memset接受任意类型指针

int IntArray_a[100];

memset (IntArray_a, 0, 100*sizeof(int) );//将IntArray_a清0

例子:memcpy接受任意类型指针

int destIntArray_a[100],srcintarray_a[100];

//将srcintarray_a拷贝给destIntArray_a

memcpy (destIntArray_a, srcintarray_a,100*sizeof(int) );

有趣的是,memcpy和memset函数返回的也是void *类型,标准库函数的编写者都不是一

般人。

 

 

5、 void不能代表一个真实的变量。

因为定义变量时必须分配内存空间,定义void类型变量,编译器到底分配多大的内存呢。

下面代码都企图让void代表一个真实的变量,因此都是错误的代码:

void a; //错误

function(void a); //错误

void体现了一种抽象

 

 

6、一个定义为volatile变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:

1.并行设备的硬件寄存器(如:状态寄存器)

2.一个中断服务子程序中会访问到的非自动变量(Non-automaticvariables)

3.多线程应用中被几个任务共享的变量

这是区分C程序员和嵌入式系统程序员的最基本的问题:嵌入式系统程序员经常同硬件、中断、RTOS等等打交道,所有这些都要求使用volatile变量。不懂得volatile内容将会带来灾难。

假设被面试者正确地回答了这是问题(嗯,怀疑是否会是这样),我将稍微深究一下,看一下这家伙是不是真正懂得volatile完全的重要性。

1.一个参数既可以是const还可以是volatile吗?解释为什么。

2.一个指针可以是volatile吗?解释为什么。

3.下面的函数被用来计算某个整数的平方,它能实现预期设计目标吗?如果不能,试回答存在什么问题:

1

2

3

4

intsquare(volatileint*ptr)

{

return*ptr**ptr;

}

下面是答案:

1.是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。

2.是的。尽管这并不很常见。一个例子是当一个中断服务子程序修改一个指向一个buffer指针时。

3.这段代码是个恶作剧。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:

1

2

3

4

5

6

7

intsquare(volatileint*ptr)

{

inta,b;

a=*ptr;

b=*ptr;

returna*b;

}

由于*ptr的值可能在两次取值语句之间发生改变,因此ab可能是不同的。结果,这段代码可能返回的不是你所期望的平方值!正确的代码如下:

1

2

3

4

5

6

longsquare(volatileint*ptr)

{

inta;

a=*ptr;

returna*a;

}

 

 

7、 大小端问题值得注意:跟处理器有关,可以使用程序判定。

 

 

8、 enum

  在编译阶段确定其值

 

 

9、 const修饰的只读变量不能用来作为定义数组的维数,

也不能放在case关键字后面。

 

1、 Strlen和sizeof的区别

Strlen是一个函数,sizeof是一个运算符。

1、 sizeof(...)是运算符,在头文件中typedefunsigned int,其值在编译时即计算好了,参数可以是数组、指针、类型、对象、函数等。
    
它的功能是:获得保证能容纳实现所建立的最大对象的字节大小。
    
由于在编译时计算,因此sizeof不能用来返回动态分配的内存空间的大小。实际上,用sizeof来返回类型以及静态分配的对象、结构或数组所占的空间,返回值跟对象、结构、数组所存储的内容没有关系。
    
具体而言,当参数分别如下时,sizeof返回的值表示的含义如下:
    
数组——编译时分配的数组空间大小;
    
指针——存储该指针所用的空间大小(存储该指针的地址的长度,是长整型,应该为4);
    
类型——该类型所占的空间大小;
    
对象——对象的实际占用空间大小;
    
函数——函数的返回类型所占的空间大小。函数的返回类型不能是void

2、  strlen(...)是函数,要在运行时才能计算。参数必须是字符型指针(char*)。当数组名作为参数传入时,实际上数组就退化成指针了。
    
它的功能是:返回字符串的长度。该字符串可能是自己定义的,也可能是内存中随机的,该函数实际完成的功能是从代表该字符串的第一个地址开始遍历,直到遇到结束符NULL。返回的长度大小不包括NULL

3、 实际例子:

char arr[10] = "What?";
             int len_one = strlen(arr);

             int len_two = sizeof(arr); 
             cout << len_one << " and " <<len_two << endl; 

返回值为5 and 10,因为strlen计算的是字符串已经用掉的长度,因此应该为5,而sizeof返回的是获得保证能容纳实现所建立的最大对象的字节大小,这就表示的是这个数组的长度为10

char*t1[20];

   char (*t2)[20];

   printf("%d%d\n0",sizeof(t1),sizeof(t2));

返回值为80和4。*t1[20]是一个指针数组,本质上是一个数组,他表示一个长为20的数组,数组的每一位是一个指针,因此sizeof(t1)相当于在求一个数组的长度,而这个数组每一位所占的空间是一个指针的大小为4,因此总大小为80;(*t2)[20]是一个数组指针,本质上是一个指针,每个指针下面有20个空间大小,因此sizeof(t2)相当于t2[0]是一个指针,因此所占的大小为4。

 

2、 如何计算结构体的大小

首先需要明确,在c中,空结构体

structpoint{};

所占的大小为0,在c++中所占的大小为1。

structpoint{

   int num;

   char k;

   int c;

};

structpoint p;

   printf("%d\n",sizeof(p));

返回结果为12。这里我们先要明确一点:如何计算结构体的大小。运算符sizeof可以计算出给定类型的大小,对于32位系统来说,

sizeof(char) = 1; sizeof(int) = 4

基本数据类型的大小很好计算,我们来看一下如何计算构造数据类型的大小。

    C语言中的构造数据类型有三种:数组、结构体和共用体。

数组是相同类型的元素的集合,只要会计算单个元素的大小,整个数组所占空间等于基础元素大小乘上元素的个数。

结构体中的成员可以是不同的数据类型,成员按照定义时的顺序依次存储在连续的内存空间。和数组不一样的是,结构体的大小不是所有成员大小简单的相加,需要考虑到系统在存储结构体变量时的地址对齐问题。看下面这样的一个结构体:

       structstu1

       {

       int    i;

       char c;

       int j;

       }

先介绍一个相关的概念——偏移量。偏移量指的是结构体变量中成员的地址和结构体变量地址的差。结构体大小等于最后一个成员的偏移量加上最后一个成员的大小。显然,结构体变量中第一个成员的地址就是结构体变量的首地址。因此,第一个成员i的偏移量为0。第二个成员c的偏移量是第一个成员的偏移量加上第一个成员的大小(0+4,其值为4;第三个成员j的偏移量是第二个成员的偏移量加上第二个成员的大小(4+1,其值为5

实际上,由于存储变量时地址对齐的要求,编译器在编译程序时会遵循两条原则:

一、结构体变量中成员的偏移量必须是成员大小的整数倍(0被认为是任何数的整数倍)

二、结构体大小必须是所有成员大小的整数倍。

对照第一条,上面的例子中前两个成员的偏移量都满足要求,但第三个成员的偏移量为5,并不是自身(int)大小的整数倍。编译器在处理时会在第二个成员后面补上3个空字节,使得第三个成员的偏移量变成8

对照第二条,结构体大小等于最后一个成员的偏移量加上其大小,上面的例子中计算出来的大小为12,满足要求。

再看一个满足第一条,不满足第二条的情况

struct stu2

       {

       int   k;

       short t;

}

成员k的偏移量为0;成员t的偏移量为4,都不需要调整。但计算出来的大小为6,显然不是成员k大小的整数倍。因此,编译器会在成员t后面补上2个字节,使得结构体的大小变成8从而满足第二个要求。

3、 算术运算符 > 关系运算符 > 赋值运算符

因此:

X > y+2 也意味着x>(y+2)

X=y>2  也意味着 x=(y>2)

 

4、 在c语言中所有的输入实际上是一个输入流,可以用getchar来接收。

5、 内存分配方式:

内存分配方式有三种:
1从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
2在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。(在函数中不要返回栈内存,但可以返回动态分配的内存)。
3从堆上分配,亦称动态内存分配。程序在运行的时候用mallocnew 申请任意多少的内存,程序员自己负责在何时用freedelete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。

例子:
1

voidGetMemory(char*p)
{
     p = (char *)malloc(100);
}
void Test(void)
{
    char *str = NULL;
    GetMemory(str);
    strcpy(str, "hello world");
    printf(str);
}
请问运行Test 函数会有什么样的结果?
答:程序崩溃(段错误)。因为GetMemory并不能传递动态内存,Test函数中的str一直都是 NULLstrcpy(str,"helloworld");将使程序崩溃。

2
char *GetMemory(void)
{
    char p = "hello world";
    return p;
}
void Test(void)
{
    char *str = NULL;
    str = GetMemory();
    printf(str);
}
请问运行Test 函数会有什么样的结果?
答:可能是乱码。
因为GetMemory返回的是指向栈内存的指针,该指针的地址不是 NULL,但其原现的内容已经被清除,新内容不可知。

3
void GetMemory2(char **p, int num)
{
    *p = (char *)malloc(num);
}
void Test(void)
{
    char *str = NULL;
    GetMemory(&str, 100);
    strcpy(str, "hello");
    printf(str);
}
请问运行Test 函数会有什么样的结果?
答:
1)能够输出hello
2)内存泄漏。

4
void Test(void)
{
    char *str = (char *) malloc(100);
    strcpy(str, “hello”);
    free(str);//
没有将str置为NULL
if(str != NULL)
{
    strcpy(str, “world”);
    printf(str);
}
}
请问运行Test 函数会有什么样的结果?
答:篡改动态内存区的内容,后果难以预料,非常危险。
因为free(str);之后,str成为野指针,
if(str != NULL)
语句不起作用。

 

6、 数组名实际上是该数组首元素的地址,因此可以这么定义:

Char *p,a[10];       p = a;

但是和数组不同,一个结构的名字不是该结构的地址,因此必须使用&,例如:

Structguy *him, bar;       him = &bar;

 

7、 复杂的类型判断中需要记住:

1.    表示一个数组的[ ]和表示一个函数的()具有同样的优先级,这个优先级高于间接运算符 *的优先级。

2.    [ ]()都是从左往右进行结合的。

所以,如下所示:

Int * risks[10]: 具有10个元素的数组,每个元素是一个指向Int的指针。

Int (*risks)[10]: 一个指针,指向具有10个元素的数组

Int *oof[3][4]: 一个3*4的数组,每个元素是一个指向int的指针。

Int (*uuf)[3][4]: 一个指针,指向3*4int数组

Int (*uuf[3])[4]: 一个具有3个元素的数组,每个元素是一个指向具有4个元素的int数组的指针

Typedef char(* frptc())[5]: frptc是一个函数,该函数返回一个指向含有5个元素的char数组的指针

 

8、 函数指针:void (*pf)( );

指针函数:void *pf();

使用函数名的所有四种方法:

定义函数;声明函数;调用函数;作为指针

 

 

 

9、 Register关键字

 register:这个关键字请求编译器尽可能的将变量存在CPU内部寄存器中,而不是通过内存寻址访问,以提高效率注意是尽可能,不是绝对。你想想,一个CPU的寄存器也就那么几个或几十个,你要是定义了很多很多register变量,它累死也可能不能全部把这些变量放入寄存器吧,轮也可能轮不到你。

一、皇帝身边的小太监----寄存器

    不知道什么是寄存器?那见过太监没有?没有?其实我也没有。没见过不要紧,见过就麻烦大了。^_^,大家都看过古装戏,那些皇帝们要阅读奏章的时候,大臣总是先将奏章交给皇帝旁边的小太监,小太监呢再交给皇帝同志处理。这个小太监只是个中转站,并无别的功能。

    
好,那我们再联想到我们的CPUCPU不就是我们的皇帝同志么?大臣就相当于我们的内存,数据从他这拿出来。那小太监就是我们的寄存器了(这里先不考虑CPU的高速缓存区)。数据从内存里拿出来先放到寄存器,然后CPU再从寄存器里读取数据来处理,处理完后同样把数据通过寄存器存放到内存里,CPU不直接和内存打交道。这里要说明的一点是:小太监是主动的从大臣手里接过奏章,然后主动的交给皇帝同志,但寄存器没这么自觉,它从不主动干什么事。一个皇帝可能有好些小太监,那么一个CPU也可以有很多寄存器,不同型号的CPU拥有寄存器的数量不一样。

    
为啥要这么麻烦啊?速度!就是因为速度。寄存器其实就是一块一块小的存储空间,只不过其存取速度要比内存快得多。进水楼台先得月嘛,它离CPU很近,CPU一伸手就拿到数据了,比在那么大的一块内存里去寻找某个地址上的数据是不是快多了?那有人问既然它速度那么快,那我们的内存硬盘都改成寄存器得了呗。我要说的是:你真有钱!

 

二、举例

    register修饰符暗示编译程序相应的变量将被频繁地使用,如果可能的话,应将其保存在CPU的寄存器中,以加快其存储速度。例如下面的内存块拷贝代码,

#ifdefNOSTRUCTASSIGN

memcpy(d, s, l)

{

        register char *d;

    register char *s;

    register int i;

    while (i--)

        *d++ = *s++;

}

#endif


三、使用register修饰符的注意点

    但是使用register修饰符有几点限制。

  首先,register变量必须是能被CPU所接受的类型。这通常意味着register变量必须是一个单个的值,并且长度应该小于或者等于整型的长度不过,有些机器的寄存器也能存放浮点数。

  其次,因为register变量可能不存放在内存中,所以不能用“&”来获取register变量的地址

  由于寄存器的数量有限,而且某些寄存器只能接受特定类型的数据(如指针和浮点数),因此真正起作用的register修饰符的数目和类型都依赖于运行程序的机器,而任何多余的register修饰符都将被编译程序所忽略。

  在某些情况下,把变量保存在寄存器中反而会降低程序的运行速度。因为被占用的寄存器不能再用于其它目的;或者变量被使用的次数不够多,不足以装入和存储变量所带来的额外开销。

  早期的C编译程序不会把变量保存在寄存器中,除非你命令它这样做,这时register修饰符是C语言的一种很有价值的补充。然而,随着编译程序设计技术的进步,在决定那些变量应该被存到寄存器中时,现在的C编译环境能比程序员做出更好的决定。实际上,许多编译程序都会忽略register修饰符,因为尽管它完全合法,但它仅仅是暗示而不是命令

 

10、          可变宏和可变参数

可变参数函数实现原理:

首先在介绍可变参数表函数的设计之前,我们先来介绍一下最经典的可变参数表printf函数的实现原理。
一、printf函数的实现原理
C/C++中,对函数参数的扫描是从后向前的。C/C++的函数参数是通过压入堆栈的方式来给函数传参数的(堆栈是一种先进后出的数据结构),最先压入的参数最后出来,在计算机的内存中,数据有2块,一块是堆,一块是栈(函数参数及局部变量在这里),而栈是从内存的高地址向低地址生长的,控制生长的就是堆栈指针了,最先压入的参数是在最上面,就是说在所有参数的最后面,最后压入的参数在最下面,结构上看起来是第一个,所以最后压入的参数总是能够被函数找到,因为它就在堆栈指针的上方。printf的第一个被找到的参数就是那个字符指针,就是被双引号括起来的那一部分,函数通过判断字符串里控制参数的个数来判断参数个数及数据类型,通过这些就可算出数据需要的堆栈指针的偏移量了,下面给出printf("%d,%d",a,b);(其中ab都是int型的)的汇编代码

[cpp] view plaincopy

1.  .section  

2.  .data  

3.  string out = "%d,%d"  

4.  push b  

5.  push a  

6.  push $out  

7.  call printf  

[cpp] view plaincopy

1.  .section  

2.  .data  

3.  string out = "%d,%d"  

4.  push b  

5.  push a  

6.  push $out  

7.  call printf  

你会看到,参数是最后的先压入栈中,最先的后压入栈中,参数控制的那个字符串常量是最后被压入的,所以这个常量总是能被找到的。
二、可变参数表函数的设计
     
标准库提供的一些参数的数目可以有变化的函数。例如我们很熟悉的printf,它需要有一个格式串,还应根据需要为它提供任意多个其他参数。这种函数被称作具有变长度参数表的函数,或简称为变参数函数。我们写程序中有时也可能需要定义这种函数。要定义这类函数,就必须使用标准头文件<stdarg.h>,使用该文件提供的一套机制,并需要按照规定的定义方式工作。本节介绍这个头文件提供的有关功能,它们的意义和使用,并用例子说明这类函数的定义方法。
      C
中变长实参头文件stdarg.h提供了一个数据类型va-list和三个宏(va-startva-argva-end),用它们在被调用函数不知道参数个数和类型时对可变参数表进行测试,从而为访问可变参数提供了方便且有效的方法。va-list是一个char类型的指针,当被调用函数使用一个可变参数时,它声明一个类型为va-list的变量,该变量用来指向va-argva-end所需信息的位置。下面给出va_listC中的源码:

[cpp] view plaincopy

1.  typedef char *  va_list;  

[cpp] view plaincopy

1.  typedef char *  va_list;  

     void va-start(va-list ap,lastfix)是一个宏,它使va-list类型变量ap指向被传递给函数的可变参数表中的第一个参数,在第一次调用va-argva-end之前,必须首先调用该宏。va-start的第二个参数lastfix是传递给被调用函数的最后一个固定参数的标识符。va-start使ap只指向lastfix之外的可变参数表中的第一个参数,很明显它先得到第一个参数内存地址,然后又加上这个参数的内存大小,就是下个参数的内存地址了。下面给出va_startC中的源码:

[cpp] view plaincopy

1.  #define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )   

2.  #define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )   //得到可变参数中第一个参数的首地址  

[cpp] view plaincopy

1.  #define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )  

2.  #define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )   //得到可变参数中第一个参数的首地址  

      type va-arg(va-list ap,type)也是一个宏,其使用有双重目的,第一个是返回ap所指对象的值,第二个是修改参数指针ap使其增加以指向表中下一个参数。va-arg的第二个参数提供了修改参数指针所必需的信息。在第一次使用va-arg时,它返回可变参数表中的第一个参数,后续的调用都返回表中的下一个参数,下面给出va_argC中的源码:

[cpp] view plaincopy

1.  #define va_arg(ap,type)    ( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) )    //将参数转换成需要的类型,并使ap指向下一个参数  

[cpp] view plaincopy

1.  #define va_arg(ap,type)    ( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) )    //将参数转换成需要的类型,并使ap指向下一个参数  

     在使用va-arg时,要注意第二个参数所用类型名应与传递到堆栈的参数的字节数对应,以保证能对不同类型的可变参数进行正确地寻址,比如实参依次为char型、char * 型、int型和float型时,在va-arg中它们的类型则应分别为intchar *intdouble.
     void va-end(va-list ap)
也是一个宏,该宏用于被调用函数完成正常返回,功能就是把指针ap赋值为0,使它不指向内存的变量。下面给出va_endC中的源码:

[cpp] view plaincopy

1.  #define va_end(ap)      ( ap = (va_list)0 )  

[cpp] view plaincopy

1.  #define va_end(ap)      ( ap = (va_list)0 )  

     va-end必须在va-arg读完所有参数后再调用,否则会产生意想不到的后果。特别地,当可变参数表函数在程序执行过程中不止一次被调用时,在函数体每次处理完可变参数表之后必须调用一次va-end,以保证正确地恢复栈。
   
一个变参数函数至少需要有一个普通参数,其普通参数可以具有任何类型。在函数定义中,这种函数的最后一个普通参数除了一般的用途之外,还有其他特殊用途。下面从一个例子开始说明有关的问题。
假设我们想定义一个函数sum,它可以用任意多个整数类型的表达式作为参数进行调用,希望sum能求出这些参数的和。这时我们应该将sum定义为一个只有一个普通参数,并具有变长度参数表的函数,这个函数的头部应该是(函数原型与此类似):
int sum(int n, ...)

我们实际上要求在函数调用时,从第一个参数n得到被求和的表达式个数,从其余参数得到被求和的表达式。在参数表最后连续写三个圆点符号,说明这个函数具有可变数目的参数。凡参数表具有这种形式(最后写三个圆点),就表示定义的是一个变参数函数。注意,这样的三个圆点只能放在参数表最后,在所有普通参数之后。
下面假设函数sum里所用的va_list类型的变量的名字是vap。在能够用vap访问实际参数之前,必须首先用宏a_start对这个变量进行初始化。宏va_start的类型特征可以大致描述为:
va_start(va_list vap, 最后一个普通参数)
在函数sum里对vap初始化的语句应当写为:

va_start(vap, n); 相当于  char*vap= (char *)&n + sizeof(int);
此时vap正好指向n后面的可变参数表中的第一个参数。

在完成这个初始化之后,我们就可以通过另一个宏va_arg访问函数调用的各个实际参数了。宏va_arg的类型特征可以大致地描述为:
类型 va_arg(va_list vap,类型名)
在调用宏va_arg时必须提供有关实参的实际类型,这一类型也将成为这个宏调用的返回值类型。对va_arg的调用不仅返回了一个实际参数的值(当前实际参数的值),同时还完成了某种更新操作,使对这个宏va_arg的下次调用能得到下一个实际参数。对于我们的例子,其中对宏va_arg的一次调用应当写为:
v = va_arg(vap, int);
这里假定v是一个有定义的int类型变量。
在变参数函数的定义里,函数退出之前必须做一次结束动作。这个动作通过对局部的va_list变量调用宏va_end完成。这个宏的类型特征大致是:
void va_end(va_list vap);
三、栈中参数分布以及宏使用后的指针变化说明如下:

下面是函数sum的完整定义,从中可以看到各有关部分的写法:

[cpp] view plaincopy

1.  #include<iostream>   

2.  using namespace std;  

3.  #include<stdarg.h>   

4.    

5.  int sum(int n,...)  

6.  {  

7.      int i , sum = 0;  

8.      va_list vap;  

9.      va_start(vap , n);     //指向可变参数表中的第一个参数   

10.     for(i = 0 ; i < n ; ++i)  

11.         sum += va_arg(vap , int);     //取出可变参数表中的参数,并修改参数指针vap使其增加以指向表中下一个参数   

12.     va_end(vap);    //把指针vap赋值为  

13.     return sum;  

14. }  

15. int main(void)  

16. {  

17.     int m = sum(3 , 45 , 89 , 72);  

18.     cout<<m<<endl;  

19.     return 0;  

20. }  

[cpp] view plaincopy

1.  #include<iostream>  

2.  using namespace std;  

3.  #include<stdarg.h>  

4.    

5.  int sum(int n,...)  

6.  {  

7.      int i , sum = 0;  

8.      va_list vap;  

9.      va_start(vap , n);     //指向可变参数表中的第一个参数  

10.     for(i = 0 ; i < n ; ++i)  

11.         sum += va_arg(vap , int);     //取出可变参数表中的参数,并修改参数指针vap使其增加以指向表中下一个参数  

12.     va_end(vap);    //把指针vap赋值为0  

13.     return sum;  

14. }  

15. int main(void)  

16. {  

17.     int m = sum(3 , 45 , 89 , 72);  

18.     cout<<m<<endl;  

19.     return 0;  

20. }  

这里首先定义了va_list变量vap,而后对它初始化。循环中通过va_arg取得顺序的各个实参的值,并将它们加入总和。最后调用va_end结束。
下面是调用这个函数的几个例子:
k = sum(3, 5+8, 7, 26*4);
m = sum(4, k, k*(k-15), 27, (k*k)/30);
函数sum中首先定义了可变参数表指针vap,而后通过va_start ( vap, n )取得了参数表首地址(赋值给了vap),其后的for循环则用来遍历可变参数表。这种遍历方式与我们在数据结构教材中经常看到的遍历方式是类似的。
函数sum看起来简洁明了,但是实际上printf的实现却远比这复杂。sum函数之所以看起来简单,是因为:
1sum函数可变参数表的长度是已知的,通过num参数传入;
2sum函数可变参数表中参数的类型是已知的,都为int型。
  而printf函数则没有这么幸运。首先,printf函数可变参数的个数不能轻易的得到,而可变参数的类型也不是固定的,需由格式字符串进行识别(由%f%d%s等确定),因此则涉及到可变参数表的更复杂应用。
在这个函数中,需通过对传入的格式字符串(首地址为lpStr)进行识别来获知可变参数个数及各个可变参数的类型,具体实现体现在for循环中。譬如,在识别为%d后,做的是va_arg ( vap, int ),而获知为%l%lf后则进行的是va_arg ( vap, long )va_arg ( vap, double )。格式字符串识别完成后,可变参数也就处理完了。
在编写和使用具有可变数目参数的函数时,有几个问题值得注意。
第一:调用va_arg将更新被操作的va_list变量(如在上例的vap),使下次调用可以得到下一个参数。在执行这个操作时,va_arg并不知道实际有几个参数,也不知道参数的实际类型,它只是按给定的类型完成工作。因此,写程序的人应在变参数函数的定义里注意控制对实际参数的处理过程。上例通过参数n提供了参数个数的信息,就是为了控制循环。标准库函数printf根据格式串中的转换描述的数目确定实际参数的个数。如果这方面信息有误,函数执行中就可能出现严重问题。编译程序无法检查这里的数据一致性问题,需要写程序的人自己负责。在前面章节里,我们一直强调对printf等函数调用时,要注意格式串与其他参数个数之间一致性,其原因就在这里。
第二:编译系统无法对变参数函数中由三个圆点代表的那些实际参数做类型检查,因为函数的头部没有给出这些参数的类型信息。因此编译处理中既不会生成必要的类型转换,也不会提供类型错误信息。考虑标准库函数printf,在调用这个函数时,不但实际参数个数可能变化,各参数的类型也可能不同,因此不可能有统一方式来描述它们的类型。对于这种参数,C语言的处理方式就是不做类型检查,要求写程序的人保证函数调用的正确性。
假设我们写出下面的函数调用:
k = sum(6, 2.4, 4, 5.72, 6, 2);

编译程序不会发现这里参数类型不对,需要做类型转换,所有实参都将直接传给函数。函数里也会按照内部定义的方式把参数都当作整数使用。编译程序也不会发现参数个数与6不符。这一调用的结果完全由编译程序和执行环境决定,得到的结果肯定不会是正确的。

四、简单的练习

问题1:可变长参数的获取
  有这样一个具有可变长参数的函数,其中有下列代码用来获取类型为float的实参:
va_arg (argp, float);
  这样做可以吗?
  答案与分析:
  不可以。在可变长参数中,应用的是"加宽"原则。也就是float类型被扩展成doublechar short类型被扩展成int。因此,如果你要去可变长参数列表中原来为float类型的参数,需要用va_arg(argp, double)。对charshort类型的则用va_arg(argp, int)
问题2:定义可变长参数的一个限制
  为什么我的编译器不允许我定义如下的函数,也就是可变长参数,但是没有任何的固定参数?

[cpp] view plaincopy

1.  int f(...)  

2.  {  

3.      ......  

4.      ......  

5.      ......  

6.  }  

[cpp] view plaincopy

1.  int f(...)  

2.  {  

3.      ......  

4.      ......  

5.      ......  

6.  }  

答案与分析:
不可以。这是ANSI C所要求的,你至少得定义一个固定参数。这个参数将被传递给va_start(),然后用va_arg()va_end()来确定所有实际调用时可变长参数的类型和值。
      
问题3:如何判别可变参数函数的参数类型?
函数形式如下:

[cpp] view plaincopy

1.  void fun(char *str ,...)  

2.  {  

3.      ......  

4.      ......  

5.      ......  

6.  }  

[cpp] view plaincopy

1.  void fun(char *str ,...)  

2.  {  

3.      ......  

4.      ......  

5.      ......  

6.  }  

若传的参数个数大于1,如何判别第2个以后传参的参数类型???
答案与分析:
这个是没有办法判断的,例如printf( "%d%c%s ",   ....)是通过格式串中的%d %c %s来确定后面参数的类型,其实你也可以参考这种方法来判断不定参数的类型。
最后,奉献上自己写的一个printf函数

[cpp] view plaincopy

1.  #include<stdio.h>   

2.  #include<stdarg.h>   

3.    

4.  void myitoa(int n, char str[], int radix)  

5.  {  

6.      int i , j , remain;  

7.      char tmp;  

8.      i = 0;  

9.      do  

10.     {  

11.         remain = n % radix;  

12.         if(remain > 9)  

13.             str[i] = remain  - 10 + 'A';  

14.         else  

15.             str[i] = remain + '0';  

16.         i++;  

17.     }while(n /= radix);  

18.     str[i] = '\0';  

19.   

20.     for(i-- , j = 0 ; j <= i ; j++ , i--)  

21.     {  

22.         tmp = str[j];  

23.         str[j] = str[i];  

24.         str[i] = tmp;  

25.     }  

26.   

27. }  

28.   

29. void myprintf(const char *format, ...)  

30. {  

31.     char c, ch, str[30];  

32.     va_list ap;  

33.   

34.     va_start(ap, format);  

35.     while((c = *format))  

36.     {  

37.         switch(c)  

38.         {  

39.         case '%':  

40.             ch = *++format;  

41.             switch(ch)  

42.             {  

43.             case 'd':  

44.                 {  

45.                     int n = va_arg(ap, int);  

46.                     myitoa(n, str, 10);  

47.                     fputs(str, stdout);  

48.                     break;  

49.                 }  

50.             case 'x':  

51.                 {  

52.                     int n = va_arg(ap, int);  

53.                     myitoa(n, str, 16);  

54.                     fputs(str, stdout);  

55.                     break;  

56.                 }  

57.             case 'f':  

58.                 {  

59.                     double f = va_arg(ap, double);  

60.                     int n;  

61.                     n = f;  

62.                     myitoa(n, str, 10);  

63.                     fputs(str, stdout);  

64.                     putchar('.');  

65.                     n = (f - n) * 1000000;  

66.                     myitoa(n, str, 10);  

67.                     fputs(str, stdout);  

68.                     break;  

69.                 }  

70.             case 'c':  

71.                 {  

72.                     putchar(va_arg(ap, int));  

73.                     break;  

74.                 }  

75.             case 's':  

76.                 {  

77.                     char *p = va_arg(ap, char *);  

78.                     fputs(p, stdout);  

79.                     break;  

80.                 }  

81.             case '%':  

82.                 {  

83.                     putchar('%');  

84.                     break;  

85.                 }  

86.             default:  

87.                 {  

88.                     fputs("format invalid!", stdout);  

89.                     break;  

90.                 }  

91.             }  

92.             break;  

93.         default:  

94.             putchar(c);  

95.             break;  

96.         }  

97.         format++;  

98.     }  

99.     va_end(ap);  

100.  }  

101.    

102.  int main(void)  

103.  {  

104.      myprintf("%d, %x, %f, %c, %s, %%,%a\n", 10, 15, 3.14, 'B'"hello");  

105.      return 0;  

106.  }  


1、Const char *s, charconst *p 和 char * const p的区别

前两个可以互换。他们声明了一个指向字符常量的指针(不能改变它所指向的字符的值);最后一个声明一个指向(可变)字符的指针常量,也就是说不能修改指针。

 

2、为什么char*能够赋值给constchar*,但是char**不能赋值给const char**

按照《c专家编程》p20页的讲解:
根据c标准:要是赋值合法,必须满足下列条件:.

两个操作数都是指向有限定符和无限定符的相容类型的指针,左边指针指向的类型必须具有右边指针所指向类型的全部限定符。

由此我们可以知道,如下的代码中:

Char *cp;

Const char *ccp;

Ccp=cp;

其中,左操作数是一个指向有const限定符的char类型的指针,右操作数是一个没有操作符的char类型的指针,所以char类型和char类型是相容的,因此可以赋值,但是反过来就不可以。

但是我们要看到:const char *p指向的是一个有const修饰符的char类型的指针,const修饰的是这个指针的类型而不是指针本身。

我们再看到char **p1和const char **p1,char **p1是一个指向没有限定符修饰的char类型的指针的指针,这句话应该解读为:指向(没有限定符修饰的char类型的指针)的指针,而const char **p2是一个指向有const修饰的char类型的指针的指针,这句话应该解读为:指向(有const修饰的char类型的指针)的指针,因此他们指针所指向的类型不一样,因此他们不相容。

按照《c prime plus》第270页的解释,把非const指针赋值给const指针式允许的,但是这样的赋值有一个前提:只能进行一层间接运算,在进行两层的间接运算时,不再安全,因为可能会出现以下的情况:

Cons tint **pp2;

Int *p1;

Const int n = 1;

Pp2 = &p1 ;     //不允许,但是我们假设允许

*pp2 = &n;     //合法,二者都是const,但这同时会使p1指向n

*p1 = 10;     //合法,但是这将改变constn的值

 

3、void main()的定义是错误的。

 

4、p = n * sizeof * q;

这里只有一个乘号,因为当sizeof的操作数是个类型名时,两边必须加上括号;当操作数如果不是类型名则不必加。

 

5、左值右值的问题

X = y;

在这个上下文中,x代表的意思是x所代表的地址;这被称为左值;左值在编译时可知,左值表示存储结果的地方。

Y表示y所代表的地址的内容;这被称为右值;右值直到运行时才知道,如无特殊说明,右值表示y的内容。

C语言中引入了“可修改的左值”的概念。他表示左值允许出现在赋值语句的左边。这个设定是为了与数组名分开,因为数组名也表示对象在内训中的地址,也是左值,但他不是可修改的左值,因此不能给数组赋值。

编译器为每个变量分配一个地址(左值)。这个地址在编译时可知,而且该变量在运行时一直保存于这个地址,相反存储在变量中的值(右值)只有在运行时才可知。

 

6、数组和指针的区别(c专家编程p87)

数组和指针都可以在他们的定义中使用字符串常量初始化,但是他们底层的实现机制不一样。

定义指针时,编译器并不为指针所指向的对象分配空间,他只是分配指针本身的空间,除非在定义指针时同时赋给指针一个字符串常量进行初始化,如下:

Char *p = “abcde”;

注意只有对字符串常量才能如此,如果是浮点数什么的就不可以,如下:

Float *f = 2.0;      //错误

在c中,初始化指针时所创建的字符串常量被定义为只读(在vc6.0中测试,对字符串指针某一个做修改不会报错,但是在运行的时候会退出)。如果通过程序修改指针指向的字符串的值,程序就会出现未定义的行为。在有些编译器中,字符串常量被存放在只允许读取的文本段中,以防止被修改。

与指针相反,由字符串常量初始化的数组是可以修改的。

 

7、局部变量:    栈区局部静态变量:静态区全局变量:    静态区的常量区全局静态变量:静态区内存分四个区:静态区,栈区,堆区,代码区

 

8、c程序优化(c专家编程p149)

在cpu和主存之间存在cache,写代码时让尽量在cache中命中有利于提高程序性能。

先看两个程序:

#include<stdio.h>

#include<stdlib.h>

#include<string.h>

#include<time.h>

 

#definedumpCopy for(i = 0; i < 65536; i++) destination[i] = source[i]

#definesmartCopy memcpy(destination, source, 65536)

 

intmain(){

         clock_t start, end;

         char source[65536], destination[65536];

         int i, j;

         start = clock();

         for(j = 0; j < 10000; j++){

                  smartCopy;

         }

         end = clock();

         printf("%d\n",end - start);

         printf("The time was: %f\n",(double)(end - start) / CLK_TCK);

         return 0;

}

程序中有dumpcopy和smartcopy两个宏定义。使用dumpcopy的方法显然降低了程序的拷贝性能。这是因为smartcopy尽量在cache中命中引起的。在别的机器上没有试过,但是在我么机器上是这样。书上解释了是因为对标签存储的优化引起的。在这种设计方法中,只有地址的高位才被放入标签中。这样一来,source和destination便不能同时出现在cache中,导致了性能的下降。

库函数memcpy进过特别优化来提高性能,他把先读取一个cache行然后再对他进行写入这个过程分解开来,就避免了上述问题。

 

9、经常引起段错误的原因

(1)坏指针值错误:在指针赋值前就用它来引用内存;向库函数传送一个坏指针;对指针进行释放后再访问他的内容。  Free(p); p = NULL;

(2)改写错误:越过数组边界写入数据;在动态分配的内存之外写数据;改写一些堆管理数据结构

(3)指针释放引起的错误:释放同一个内存块两次;释放一块未曾使用的malloc分配的内存;释放仍在使用的内存;释放一个无效的指针。

例如这样:for(p = start; p; p = p->next),在这样的循环中迭代一个链表,并在循环体内使用free(p)语句,这样,在洗一次迭代循环时,程序就会对已经释放的指针进行解除引用的操作,从而导致不可预料的结果。

其实,最好的方式如下:

*p,*start,*tmp;

For(p= start; p; p = tmp){

Tmp= p->next;

Free(p);

}

 

10

Float:

1bit(符号位)   8bit(指数位)   23bit(尾数位)

Double

1bit(符号位)   11bit(指数位)  52bit(尾数位)

 

 

11悬挂else的时候应该要注意是否会出错(c陷阱与缺陷p30)

 

12c语言中只有一维数组

 

13c陷阱与缺陷p44

在c语言中将一个整数转化为一个指针,最后得到的结构取决于具体的c编译器实现。这个特殊的常量就是0,编译器保证由0转化过来的指针不等于任何有效的指针。其实在C中,NULL的定义如下所示:

#defineNULL 0

所以无论是直接用0,还是null都是对的,有一点需要记住:当常熟0被转换为指针使用时,这个指针绝对不能被解除引用,也就是说当我们将0赋值给一个指针变量时,绝对不能企图使用该指针所指向的内存中的内容,下面的写法是合法的:

If(p== (char*)0)

但是

If(strcmp(p,(char*)0) == 0)这种写法是非法的,因为strcmp的实现中会包括查看他的指针所指向的内容的操作

 

P110:NULL指针并不指向任何对象,除非是赋值或比较运算符,其余的任何操作都是未定义的,也就是说可以用来赋值,比较,但是不能取地址为0的值,这在vc6.0上会指向失败。

 

14getchar()的返回值类型为int

 

15右移位操作的时候有符号整数既可以用0填充空出的位,也可以用符号位的副本填充空出的位。无符号数是用0填充。

移位操作要求移位必须大于等于0,但必须小于被移位的对象的长度。

 

16c陷阱与缺陷p112

除法运算时发生的截断:

假设q = a / b; r = a % b;我们希望:

(1)     q*b + r == a;

(2)     如果我们改变a的正负号,那么q的正负号也会改变,但是q绝对值不变

(3)     B>0时,我们希望保证r>=0且r<b。例如取余的时候。

这样的话,如果(-3) / 2,如果结果是-1,那就取余是-1,不满足第三条;如果余数是1,那就商是-2,不满足第二条。因此,在大多数的编译器中,会首先满足第二条,vc6.0也是这样。但是c语言的定义只保证了性质1,以及当a>=0且b>0时,保证|r|<|b|以及r>=0

 

 

怎么判断一段程序是由c写的还是c++写的

#ifdef__cplusplus

         printf("c++\n");

#else

         printf("c\n");

#endif

 

 

论述含参数的宏与函数的优缺点

答:  带参宏  函数

处理时间  编译时  程序运行时

参数类型  没有参数类型问题  定义实参、形参类型

处理过程  不分配内存  分配内存

程序长度  变长  不变

运行速度  不占运行时间  调用和返回占用时间

 

交换两个值

A=a +b b=a-b a=a-b

A=a^bb=a^b a=a^b

A^=b^=a

 

 

宏定义求解结构体中某个变量相对于结构体的偏移,例如:

Structstruc{

   Int a;

   Char b[20];

   Double c

};

则FIND(struc,a) = 0, FIND(struc, b) = 4;

可以这样定义: #define FIND(struc,e)(size_t)&(((struc*)0)->e)

其中(struc*)0表示将常量0强制转化为struc*指针所指向的地址,&(((struc*)0)->e)相当于取其成员变量的地址,size_t是一个数据类型。

 

 

 

Inta = 10;

Floatb = 1.0f;

Count<<(int)b       输出的应该是1

Cout<<(int&)b       输出的和(int*)&b相同,是地址强行转换

 

 

 

 

 

 

 

#include<stdio.h>

 

Intmain()

{

Unsigned int a = oxFFFFFFF7;

Unsigned char b = (unsigned char)a;

Unsigned char *c = (unsignedchar*)&a;

Printf(“%08x  %08x\n”,b,*c);

Return 0;

}

在这个函数中,b是将a强制转化为char型,会发生字节截断;

但是在c中,&a相当于指针,也就是说相当于

Unsignedint *p = &a;

Unsignedchar *c = (unsigned char*)p;

只是相当于把一个int型指针强制转化为char型的指针,并没有发生int到char的类型转换。

 

 

内联函数和宏的区别

内联函数是函数,宏不是函数

内联函数是在调用的时候直接被镶嵌到目标代码中,而宏定义则是做了一个简单的替换。

内联函数要做参数类型检查

 

 

 

Intarray[3]

&array指的是取得是array数组的地址,而不是其中的某个值,因此:

&array又可以==(*array)[3]

 

 

Typedefstruct woman_tag woman;

Structman{

    Woman *w;

};

Structwoman_tag{

    Struct man *m;

};

这里在woman被声明的时候,还不知道其中的内容,因此无法确定其大小,这就叫做不完全类型。

因为是不完全类型,因此不能把它变为数组,也不能将其变为结构体成员,或者声明为变量。但是仅仅用作指针是可以使用不完全类型的。Man就是使用了woman指针作为它的成员。

在c中,void也是不完全类型。

Siaeof只是在编译阶段得到了占内存的大小,所以ertern过来的不能计算sizeof

 

 

Segmentationfault: 段错误

 

 

Inline:

不应该将包含循环,switch,if的函数定义为inline,inline可以取址

Inline修饰符并不是强制性的,编译器有可能置之不理。比如说递归函数通常不会作为inline

Inline经常被写在头文件里

在没有被声明为static的函数内部,不应该定义静态存储对象

在一个c文件中定义的inline函数是不能直接在别的c文件中使用的

过度使用inline,会使得文件变大,效率变低,因为cpu使用cache,较小的文件更容易被命中

如果某个A函数未定义为inline,并且被很多其它函数调用,那个这个A函数很大的可能会长期被保存在cahe中,这样CPU对代码的执行速度会提高很多。如果A函数被定义为了inline函数,代码分散各个调用函数中,这样每次指定都不命中都需要去内存把代码拷贝到cache中,然后执行,造成很大的抖动。

 inline必须用于函数定义,对于函数声明,inline不起作用

inline定义的函数 和 宏定义一样,只在本地文件可见。所以建议Inline定义的函数放在头文件中

1.static inline --->编译器本地展开。

   2. inline--->本地展开,外地为Inline函数生成独立的汇编代码

   3. extern inline--->不会生成独立的汇编代码。

特性1.即使是通过指针应用或者是递归调用也不会让编译器为它生成汇编码,在这种时候对此函数的调用会被处理成一个外部引用

       特性2.externinline的函数允许和外部函数重名,即在存在一个外部定义的全局库函数的情况下,再定义一个同名的externinline函数也是合法的。

externinline的用处:

 

在一个库函数的c文件内,定义一个普通版本的库函数foo

mylib.c:

    void foo()

     {   

          ...;

      }

 

然后再在其头文件内,定义(注意不是声明!)一个实现相同的exterininline的版本:

 

mylib.h:

       externinline foo()

      {   

            ...;

       }

那么在别的文件要使用这个库函数的时候,只要includemylib.h,在能内联展开的地方,编译器都会使用头文件内externinline的版本来展开。

而在无法展开的时候(函数指针引用等情况),编译器就会引用mylib.c中的那个独立编译的普通版本。

即看起来似乎是个可以在外部被内联的函数一样,所以这应该是gccexterninline意义的由来。

 

 

C语言类型:

函数类型,对象类型和不完整类型

从函数类型是不能派生出除了指针类型之外的其他任何类型的

从数组类型是不能派生出函数类型的

函数类型无法计算大小。

 

 

Int的大小不是依赖于硬件,而是依赖于处理环境

 

当要把指针赋为空值,没有使用NULL,而是使用0的时候,指针能不能作为空指针来使用最终是取决于运行环境的。

看一个例子:

Int*p = 3;   //错误的

Int*p = 0;   //正确的

因为在C语言中,“当常量0处于应该作为指针使用的上下文时,他就作为空指针使用”。但是有的时候,编译器也会理解不了“应该将0作为指针处理的上下文”,这些情况是:

1、没有原型声明的函数的参数

2、可变长参数函数中的可变部分的参数

 

在表达式中,数组可以被解读成“指向他的初始元素的指针”,并且在表达式中,[]和数组是没有关系的,也就是说,数组可以被解读成指向他的初始元素的指针和在后面加不加[]没有关系。

但是,仅仅在表达式中[]和数组没有关系,声明中的[],还是表达数组的意思。也就是说,生命中的[]和表达式中的[]意义完全不一样,生命中的*和表达式中的*意义也是不一样的。所以在表达式中,a+b和b+a的意义是一样的,所以*(p + i)和*(I + p)的意义也是一样的,而且p[i]也可以写成i[p]

 

C传递多维数组

 

实参

形参

数组的数组

Char a[10][8]

Char (*)[10]

指针数组

Char *c[10]

Char **c

数组指针

Char (*c)[100]

Char (*c)[100]

指针的指针

Char **c

Char **c

Inta[8][10]

传递的时候,可以分为以下几种方案:

1、f(int a[10][20])

2、f(int a[][20])或者 f(int (*a)[20])

3、f(int **a): 只有把实参的二维数组改成指针数组的情况下才可以这样做


1、 C陷阱与缺陷p6:

=不同于==: 注意这样一句语句:

While(c = ‘ ’ || c == ‘\t’ || c == ‘\n’)

   C= getc(f);

在程序中误把c == ‘ ’写成了c = ‘ ’,使得比较运算符变成了赋值运算符,而赋值运算符= 的优先级低于逻辑运算符||,因此实际上表达式是下面这种情况:

‘ ‘ || c == ‘\t’ || c == ‘\n’

因为’ ‘不等于0,因此无论c以前是何值,上述表达式求解的结果都为1,因此循环将继续进行下去直到程序结束。

2、 C prime plus p100:

增量++减量--的优先级很高,只有圆括号比他们更高

3、 优先级最高(数组下标,函数调用,操作符各结构成员操作符)单目运算符 > 双目运算符

双目运算符:算术运算符 > 移位运算符 > 关系运算符 > 逻辑运算符 > 赋值运算符 > 条件(三目)运算符

运算符

结合性

()  []  ->  .

自左向右

!  ~  ++  --  (type)  *  &  sizeof

自右向左

/  *  %

自左向右

+  -

自左向右

<<  >>

自左向右

<  <=  >  >=

自左向右

==  !=

自左向右

&

自左向右

^

自左向右

|

自左向右

&&

自左向右

||

自左向右

?:

自右向左

assignments

自右向左

,

自左向右

需要记住的最重要的两点是:

(1)    任何一个逻辑运算符的优先级低于任何一个关系运算符

(2)    移位运算符的优先级比算术运算符分低,但是比关系运算符高

(3)    关系运算符中,==和!=的优先级比<  <=  >  >=的优先级低

(4)    任何两个逻辑运算符的优先级都不一样。

 

2、c操作数求值顺序问题(c陷阱与缺陷p58)

C语言中只有四个运算符&&  ||  ?:  ,   存在规定的求值顺序。注:分隔函数参数的逗号并非逗号运算符,例如:x和y在函数f(x,y)中的求值顺序是未定义的,而在函数g((x,y))中却是确定的先x后y的操作顺序。

C语言中其他所有运算符对其操作数的求值顺序是未定义的,也就是说运算符并不保证任何运算顺序。例如如下代码:

I=0;

While(i<n)

Y[i] = x[i++];

上面的代码假设y[i]的地址将在i的自增操作执行之前被求值,但是这一点是得不到任何保证的。相反,下面这种代码是可以得到保证的:

I = 0;

While(I < n){

    Y[i] = x[i];

    I++;

}

在优先级问题上注意一点:++和*属于同一优先级,但是++比*优先级其实更高。

举例来说:

#include<stdio.h>

#include<malloc.h>

#include<string.h>

intmain(){

char *q = (char*)malloc(sizeof(char) * 4);

char c;

strcpy(q, "abc");

c = *q++;

printf("%c %d %d\n",c &c, q);

return 0;

}

这个c最终的结果为a,这就说明++优先级比*高。总结一下,[],++优先级比*更高


&运算

通常用于二进制取位操作,例如一个数&1就是取二进制的最末位。这可以用来判断一个整数的奇偶。

|运算

通常用于二进制特定位的无条件赋值,例如一个数|1就是把二进制的末位强行变为1.如果需要把二进制的末位变成0,对这个数or1之后再减1就好了,其实际意义就是把这个数强行变成最接近的偶数。

^运算

通常用于对二进制的特定一位取反,因为异或可以这样定义:0和1异或0都不变,异或1则取反。

^的逆运算是他本身。所以我们可以知道交换两个数字可以写作:

a = a ^ b;

a = a ^ b;

b = a ^ b;

!运算

其主要用于把内存中的0和1全部取反。使用时需要注意:如果!的对象是无符号整数,那么得到的值就是他与该类型上界的差,例如:

Unsigned short a = 100;

a = ~a;

printf(“%d\n”,a);

得到的值应该是65435.

 

常用操作:

 

操作

语句

去掉最后一位

101101->10110

x>>1

在最后添加一个0

101101->1011010

X<<1

在最后添加一个1

101101->1011011

X<<1 + 1

把最后一位变为0

101101->101100

X |1 - 1

把最后一位变为1

101100->101101

X | 1

最后一位取反

101101->101100

X ^ 1

把右边第k为变成1

101101->101111 k = 2

X | (1 << (k-1))

把右边第k为变成0

101101->101001 k = 3

X & ~(1<<(k-1))

右边第k为取反

101101->101001 k = 3

X ^ (1 << k-1)

取末三位

101101->101

X & 7

取末k位

101101->1101 k = 4

X & ((1<<k)-1)

取右数第k位

101101->1 k = 4

X >> (k-1) & 1

把末k位变为1

101101->101111 k =3

X | ((1 << k)-1)

末k位取反

101101->101010 k =3

X ^ ((1 << k)-1)

把右边连续的1变为0

100101111->100100000

X & x+1

把右边第一个0变为1

100101110->100101111

X or x +1

把右边连续的0变为1

11011000->11011111

X or x - 1

取右边连续的1

100101111->1111

(x ^ x+1)>>1

去掉右起第一个1的左边

100101000->1000

X & (x ^ (x-1))

 

 

n&(n-1): 将二进制最低位为1的改为0

因此有下列应用:

1. 求某一个数的二进制表示中1的个数
while (n >0 ) {
      count ++;
      n &= (n-1);
}


2.
判断一个数是否是2的方幂
n > 0 && ((n & (n - 1)) == 0 )

 

 

条件编译:

#ifdef

使用规则:

#ifdef  MAR

   执行这里的指令

#else

   执行这里的指令

#endif

#ifdef格式类似于if,差异在于预处理器不能识别标记代码块的花括号{},[]。因此必须要使用#else(如果需要)和#endif(必须存在)来标记指令块。这些条件结构可以嵌套。

#ifdef  MAR中MAR必须存在。用于判断后面的标示符是否是存在的。

同理,#ifndef是#ifdef的反义词,用于判断后面的标识符是否是未定义的。

#if 表达式  #elif  #else    #endif,#if后面加的是表达式,表达式为真则执行,而不是像前面一样是存在与否的标示。

defined(aaa),defined是一个预处理器运算符,他表示defined的参数是否已经用#define定义过,定义过则返回1,否则返回0。

#ifdef:使用这种方法调试代码; 选择适用于不同c实现的大块代码。

#ifndef: 通常用于防止多次包含同一文件

 



#define: 多用于定义明显常量

C prime p448: c编译器在编译时对所有常量表达式(只包含常量的表达式)求值,所以,实际处理过程发生在编译阶段,预处理阶段仅仅执行的是文字替换操作。

#define m 4 * 8        #define m 4*8

从技术角度看,c把宏定义的主体部分当做语言符号,语言符号以空格分隔。也就是说,第一种会被认为有三个语言符号4、*、8,第二种只有一个语言符号4*8;但是从字符串来替换的话,都只有一个符号。有些编译器把她当做字符串。

在重定义常量的时候,c规定了:只允许新定义与旧定义完全相同,也就是说:

#define m 4 * 8

#define m 4   *  8

#define m 4*8

前两种是完全相同的,因为都是三个语言符号,而后一种不行,因为只有一个语言符号。

重定义也可以只用undef指令。

要想把语言符号组织在一起,可以使用##运算符,例如:

#define m(n) x ## n

这样m(4)的结果为x4.

 

可变宏:

#define pr(…) printf(__VA_ARGS__)

可以像这样调用宏:pr(“holidauy”);

或者pr(“%d %d\n”,a,b);

也可以这样使用#define pr(x,…) printf(“message” # x “: ” __VA_ARGS__);

但是这样的调用是错的

#define wrong(x,…,y) # x # __va_args__ #y

 

#include

#include<> : 告诉预处理器在一个活多个标准系统目录中寻找文件。

#include”” : 告诉预处理器现在当前目录寻找文件

 

 

内联函数inline

用文本替代方式调用函数

#include<stdio.h>

Inline void eatline(){

  Printf(“hello\n”);

}

Int main(){

  Eatline();

}

他的函数执行过程是这样的:

#include<stdio.h>

Inline void eatline(){

  Printf(“hello\n”);

}

Int main(){

  Printf(“hello\n”);

}

并没有去调用函数,因此加快了函数的操作。

另外,内联函数没有预留给他的代码块,因此无法获得内联函数的地址(事实上可以获取,但是这样会使编译器产生不适内联函数的代码)。

编译器在优化内联函数时,必须知道函数定义的内容。这意味着内联函数的定义和对函数的调用必须在同一文件内,正因为这样,内联函数具有内部链接。如果在多文件程序中,一般会在头文件中定义内联函数,然后每个文件都包含头文件。一般不在头文件中放置可执行代码,但是内联函数是个例外。而又因为它具有内部链接,所以在多个文件中定义同一内联函数也并不会出问题。

通常c只允许程序对函数做唯一的一次定义。但是对内联函数却放宽了这个限制。因此最简单的方法是在使用内联函数的文件中都定义它。


1、 typedef和define的区别

typedef是一种彻底的封装,在声明他之后就不能再往里面添加别的东西;但是宏定义可以使用其他类型说明符对宏类型名进行拓展,例如:

#define peach int

Unsigned peach I  /*可以使用*/

Typedef int banana;

Unsigned banana I;    /*错误,非法*/unexpected tokens following preprocessor directive - expected anewline

是不支持类型的拓展,并不是不支持使用const等等,仅仅是类型拓展

(1)   typedef给出的符号名仅限于对类型,而不是对值

(2)   typedef的解释由编译器执行,而不是预处理器

(3)   虽然其范围受限,但在其范围内,他比define更灵活

Typedef并不创建新的类型,知识创建了便于使用的标签

 

 

2、 共享的名字空间(cprime p410)

C语言存在多种名字空间:标签名、标签(用于所有的结构,枚举和联合)、成员名(每个结构或联合都有自己的名字空间)、其他。

在一个特定作用域内的结构标记、联合标记和枚举标记都共享同一个名字空间,并且这个名字空间和普通变量使用的名字空间是不同的,这意味着可以在同一个作用域内对同一个变量和同一个标记使用同一个名字而不会产生错误,但不能在同一个作用域内使用名字相同的两个标记或名字相同的两个变量。

 

3、 typedef加上之后的区别

如下所湿:

Typedef struct fruit{int weight}fruit;     

Struct veg{int weight}veg;

语句1声明了结构标签fruit和由typedef声明的结构类型fruit,其实际效果如下:

Struct fuit a;

Fruit a;

语句2声明了结构标签veg和变量veg,只有结构标签能在以后的声明中使用,即

Struct veg a;

如果试图使用veg a,将会报错,因为这就类似于

Int I;

I j;



 

 

1 0
原创粉丝点击