C语言基础知识

1、关键字volatile的使用

      volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。

      volatile应该解释为“直接存取原始内存地址”比较合适,“易变的”这种解释简直有点误导人;
“易变”是因为外在因素引起的,象多线程,中断等,并不是因为用volatile修饰了的变量就是“易变”了,假如没有外因,即使用volatile定义,它也不会变化;而用volatile定义之后,其实这个变量就不会因外因而变化了,可以放心使用了;大家看看前面那种解释(易变的)是不是在误导人。


一般说来,volatile用在如下的几个地方:
1、中断服务程序中修改的供其它程序检测的变量需要加volatile
2、多任务环境下各任务间共享的标志应该加volatile
3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能有不同意义;

易错例子:

1) 一个参数既可以是const还可以是volatile吗?解释为什么。
2) 一个指针可以是volatile 吗?解释为什么。
3) 下面的函数有什么错误:
int square(volatile int *ptr)
{
   return *ptr * *ptr;
}


下面是答案:
1) 是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
2) 是的。尽管这并不很常见。一个例子是当一个中断服务子程序修改指向一个buffer的指针时。
3) 这段代码里有个恶作剧。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:
int square(volatile int *ptr)
{
  int a,b;
  a = *ptr;
  b = *ptr;
  return a * b;
}
由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返回值不是你所期望的平方值!正确的代码如下:
long square(volatile int *ptr)
{
  int a;
  a = *ptr;
  return a * a;
}

典型的例子:
for ( int i=0; i<100000; i++);
这个语句用来测试空循环的速度的;但是编译器肯定要把它优化掉,根本就不执行
如果你写成:
for ( volatile int i=0; i<100000; i++);
它就会执行了

 

2、典型的C程序存储空间布局

一个典型的C程序存储空间布局由以下几个部分组成:
    正文段:CPU执行的指令部分,也就是主要的程序代码编译出来的结果,只读,通常可以共享。
    初始化数据段:通常称之为数据段,包含了程序中需要明确赋值的变量,譬如一些初始化的全局变量等,如 int a = 10,变量名和值都存放在这个段中。
    未初始化数据段:通常称之为BSS(Block Started by Symbol)段,包含了程序中没有进行赋值的变量,譬如一些未初始化的全局变量,如 int a,在程序执行之前,内核会把这部分全部置为0(NULL),
    栈:自动变量(指在函数内部定义使用的变量)以及每次函数调用时所需保存的信息放在此段中。如函数调用时要保存返回地址等。栈是从上向下分配的。
    堆:通常在堆中进行动态存储分配,如malloc, calloc, realloc等都从这里面分配。堆是从下向上分配的。
    通常堆顶和栈底之间的虚拟地址空间是很大的。
    对X86处理器上的Linux,正文段从0x08048000开始,栈底则从0xC0000000之下开始。
    下图是一个典型的C程序存储空间的逻辑布局:

 

//main.cpp 
int a = 0; 全局初始化区 
char *p1; 全局未初始化区 
main() 
{ 
     int b; 栈 
      char s[] = "abc"; 栈 
      char *p2; 栈 
      char *p3 = "123456"; 123456/0在常量区,p3在栈上。 
      static int c =0; 全局(静态)初始化区 
      p1 = (char *)malloc(10); 
     p2 = (char *)malloc(20); 分配得来得10和20字节的区域就在堆区。 
      strcpy(p1, "123456"); 123456/0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。 
}


 

3、memset、memcpy、strcpy 的作用和区别 

(1)、memset

  原型:    extern void *memset(void *buffer, int c, int count);             

  用法:    #include <string.h>          

  功能:   把buffer所指内存区域的前count个字节设置成字符 c。          

  说明:    返回指向buffer的指针。用来对一段内存空间全部设置为某个字符

  例子:    memset(st, 0, sizeof(struct _test)*10);  //清空方法 

//memset 源码的实现 C语言
#include <mem.h> 
void* memset(void* s, int c, size_t n)
{
     unsigned char* p = (unsigned char*) s;
     while (n > 0) {
           *p++ = (unsigned char) c;
            --n;
     }
     return s;
}

(2)memcpy

   原型:extern void *memcpy(void*dest,void*src,unsignedintcount);             

  用法: #include <string.h>             

   功能: 由src所指内存区域复制count个字节到dest所指内存区域。            

   说明: src和dest所指内存区域不能重叠,函数返回指向dest的指针.可以拿它拷贝任何数据类型的对象。   

  例如:  char a[10],b[5];  memcpy(b, a, sizeof(b));             /*注意如果用sizeof(a),会造成b的内存地址溢出*/

 

(3) Strcpy  

   原型: extern char *strcpy(char *dest,char *src);                  

   用法: #include <string.h>                   

   功能: 把src所指由NULL结束的字符串复制到dest所指的数组中。              

   说明:src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳 src的字符串.返回指向dest的指针。                   

   例如:   char a[100],b[50];

             strcpy(a,b);如用   strcpy(b,a);要注意a中的字符串长度(第一个‘/0’之前)是否超过50位,如超过,则会造成b的内存地址溢出。

//来看看 strcpy的 源代码实现:
char   *strcpy(char   *strDest,const   char   *strSrc)   
{   
    assert((strDest!=NULL)&&(strSrc   !=NULL)) //判断指针是否合法,即分配内存,指向某块确定区域
     char   *address   =   strDest;            //记住目标地址的起始值
     while((*strDest++   =   *strSrc++)!='\0') //先拷贝,后判断,这样就不用在拷贝完了后,再加一句
          NULL;                                // *strDest = '/0'; -->即加一个结束符.因为字符串结束已拷贝了.
    return   address;                         //返回目标首地址的值。             
}

(4) 三者区别                    

    memset   主要应用是初始化某个内存空间。                 

    memcpy   是用于copy源空间的数据到目的空间中。                 

    strcpy   用于字符串copy,遇到‘\0’,将结束。 

 

4、sizeof与strlen的区别

1.sizeof操作符的结果类型是size_t,它在头文件中typedef为unsigned int类型。该类型保证能容纳实现所建立的最大对象的字节大小。
2.sizeof是算符,strlen是函数。

3.sizeof可以用类型做参数,strlen只能用char*做参数,且必须是以''/0''结尾的。
     sizeof还可以用函数做参数,比如:
    short f();
    printf("%d/n", sizeof( f() ));
   输出的结果是sizeof(short),即2。

4.数组做sizeof的参数不退化,传递给strlen就退化为指针了。

5.大部分编译程序 在编译的时候就把sizeof计算过了,是类型或是变量的长度。这就是sizeof(x)可以用来定义数组维数的原因
char str[20]="0123456789";
int a=strlen(str); //a=10;
int b=sizeof(str); //而b=20;

6.strlen的结果要在运行的时候才能计算出来,用来计算字符串的长度,不是类型占内存的大小。

7.sizeof后如果是类型必须加括号,如果是变量名可以不加括弧。这是因为sizeof是个操作符不是个函数。
 
8.当使用了于一个结构类型或变量时, sizeof 返回实际的大小,
 当使用一静态地空间数组, sizeof 返回全部数组的尺寸。
 sizeof 操作符不能返回被动态分配的数组或外部的数组的尺寸

 

5、不用第三个变量,交换两个数据的值

方法一:
 a = a+b;
 b = a-b;
 a = a-b;
 
方法二:
 a = a+b - (b=a);
 
方法三:
 i ^= j;
 j ^= i;
 i ^= j;
 
比较:方法一,方法二 相加减可能越界。最好采用方法三。

 

6、static与final的区别

static关键字
     static 关键字可以用来修饰类的变量,方法和内部类。static 是静态的意思,也是全局的意思它定义的东西,属于全局与类相关,不与具体实例相关。就是说它调用的时候,只是 ClassName.method(),而不是 new ClassName().method()。new ClassName()不就是一个对象了吗?static 的变量和方法不可以这样调用的。它不与具体的实例有关。

 

final关键字
final 关键字有三个东西可以修饰的。修饰类,方法,变量。  详细解释一下:
 
在类的声明中使用 final 
使用了 final 的类不能再派生子类,就是说不可以被继承了。有些 java 的面试题里面,问 String 可不可以被继承。答案是不可以,因为 java.lang.String是一个 final 类。这可以保证 String 对象方法的调用确实运行的是 String 类的方法,而不是经其子类重写后的 方法。
 
在方法声明中使用 final 
被定义为 final 的方法不能被重写了,如果定义类为 final 的话,是所有的方法都不能重写。而我们只需要类中的某几个方法,不可以被重写,就在方法前加 final 了。而且定义为 final 的方法执行效率要高的啊。
 
在变量声明中使用 final 
这样的变量就是常量了,在程序中这样的变量不可以被修改的。修改的话编译器会抱错的。而且执行效率也是比普通的变量要高。final 的变量如果没有赋予初值的话,其他方法就必需给他赋值,但只能赋值一次。

 

    
注意:子类不能重写父类的静态方法哦,也不能把父类不是静态的重写成静态的方法。想隐藏父类的静态方法的话,在子类中声明和父类相同的方法就行了。

 

7、排序法

 (1)、冒泡法

#include <stdio.h>
#include <stdlib.h>

static void bub_sort(char *buf, int num)
{
	int i,j;
	
	for (i=1; i<num; i++)
		for (j=0; j<num-i; j++)
			if (buf[j]>buf[j+1]){
				
				buf[j]   ^= buf[j+1];
				buf[j+1] ^= buf[j];
				buf[j]   ^= buf[j+1];
			}
}

int main(void)
{
	char buff[10]={0,15,26,74,11,19,59,95,23,10};
	int i;

	bub_sort(buff, 10);
	printf("\nsort:");
	for (i=0; i<10; i++)
		printf("%d ", buff[i]);	
	
	return 0;
}


(2)、二分法

//如果找到,返回buff的下标 
static int dich_sort(const char *buf, int len,int data)
{
	int i;
	int mid;
	int low=0,high=len;

	while (low <= high)
	{
		mid = (low + high)/2;
		if (buf[mid] > data)
			high = mid-1;
		else if (buf[mid] < data)
			low = mid+1;
		else
			return mid;	
	}			
	
	return -1;
}

 

6、位域


含位域结构体的sizeof:
前面已经说过,位域成员不能单独被取sizeof值,我们这里要讨论的是含有位域的结构体的sizeof,只是考虑到其特殊性而将其专门列了出来。
C99规定int、unsigned int和bool可以作为位域类型,但编译器几乎都对此作了扩展,允许其它类型类型的存在。
使用位域的主要目的是压缩存储,其大致规则为:
1) 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字
段将紧邻前一个字段存储,直到不能容纳为止;
2) 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字
段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
3) 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方
式,Dev-C++采取压缩方式;
4) 如果位域字段之间穿插着非位域字段,则不进行压缩;
5) 整个结构体的总大小为最宽基本类型成员大小的整数倍。
还是让我们来看看例子。
示例1
struct BF1
{
char f1 : 3;
char f2 : 4;
char f3 : 5;
};
其内存布局为:
|__f1___|____f2___ |__|____f3______|______|
|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|__|

位域类型为char,第1个字节仅能容纳下f1f2,所以f2被压缩到第1个字节中,而f3
能从下一个字节开始。因此sizeof(BF1)的结果为2
示例2
struct BF2
{
char f1 : 3;
short f2 : 4;
char f3 : 5;
};
由于相邻位域类型不同,在VC6中其sizeof6,在Dev-C++中为2
示例3
struct BF3
{
char f1 : 3;
char f2;
char f3 : 5;
};
非位域字段穿插在其中,不会产生压缩,在VC6Dev-C++中得到的大小均为3



运算符:

优先级:   ! > 算术运算符 > 关系运算符 > && > || > 赋值运算符


写出下列程序在X86上的运行结果。

struct mybitfields
{
unsigned short a : 4;
unsigned short b : 5;
unsigned short c : 7;
}test;

void main(void)
{
int i;
test.a=2;
test.b=3;
test.c=0;
i=*((short *)&test);
printf("%d ",i);
}

这个题的为难之处呢,就在于前面定义结构体里面用到的冒号,如果你能理解这个符号的含义,那么问题就很好解决了。这里的冒号相当于分配几位空间,也即在定义结构体的时候,分配的成员a 4位的空间, b 5位,c 7位,一共是16位,正好两个字节。下面画一个简单的示意:
变量名 位数
test 15 14 13 12 11 10 9 |8 7 6 5 4 |3 2 1 0
test.a | |0 0 1 0
test.b |0 0 0 1 1 |
test.c 0 0 0 0 0 0 0 | |
在执行i=*((short *)&test); 时,取从地址&test开始两个字节(short占两个字节)的内容转化为short型数据,即为0x0032,再转为int型为0x00000032,即50。输出的结果就是50。当然,这里还涉及到字节及位的存储顺序问题,后面再说。

前面定义的结构体被称为位结构体。所谓位结构体,是一种特殊的结构体,在需要按位访问字节或字的一个或多个位时,位结构体比按位操作要更方便一些。
位结构体的定义方式如下:
struct [位结构体名]{
数据类型 变量名:整数常数;
...
}位结构变量;
说明:
1)这里的数据类型只能为int型(包括signed和unsigned);
2)整数常数必须为0~15之间的整数,当该常数为1时,数据类型为unsigned(显然嘛,只有一位,咋表示signed?光一符号?没意义呀);
3)按数据类型变量名:整数常数;方式定义的结构成员称为位结构成员,好像也叫位域,在一个位结构体中,可以同时包含位结构成员及普通的结构成员;
4)位结构成员不能是指针或数据,但结构变量可以是指针或数据;
5)位结构体所占用的位数由各个位结构成员的位数总各决定。如在前面定义的结构体中,一共占用4+5+7=16位,两个字节。另外我们看到,在定义位结构成员时,必须指定数据类型,这个数据类型在位结构体占用多少内存时也起到不少的作用。举个例子:
struct mybitfieldA{
char a:4;
char b:3;
}testA;

struct mybitfieldB{
short a:4;
short b:3;
}testB;
这里,testA占用一个字节,而testB占用两个字节。知道原因了吧。在testA中,是以char来定义位域的,char是一个字节的,因此,位域占用的单位也按字节做单位,也即,如果不满一个字节的话按一个字节算(未定义的位按零处理)。而在testB中,short为两个字节,所以了,不满两个字节的都按两个字节算(未定义位按零处理)

(判断大端小端模式) 试题1:请写一个C函数,若处理器是Big_endian的,则返回0;若是Little_endian的,则返回1

解答:

int checkCPU( )

{

    {

           union w

           { 

                  int a;

                  char b;

           } c;

           c.a = 1;

           return(c.b ==1);

    }

}

Big-Endian    一个Word中的高位的Byte放在内存中这个Word区域的低地址处。
Little-Endian  一个Word中的低位的Byte放在内存中这个Word区域的低地址处。

 

3、在C++程序中调用被 C编译器编译后的函数,为什么要加 extern “C”? (5分)

答:C++语言支持函数重载,C语言不支持函数重载。函数被C++编译后在库中的名字与C语言的不同。假设某个函数的原型为: void foo(int x, int y);

该函数被C编译器编译后在库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字。

C++提供了C连接交换指定符号extern“C”来解决名字匹配问题。

 

 

 

 

 

 

C语言易错题

1、

        int arr[] = {6,7,8,9,10};
        int *ptr=arr;
        *(ptr++) += 123;
        printf("%d,%d\n",*ptr,*(++ptr));
从右到左的顺序

     结果为: 8,8

2、

一语句实现是不否为2的若干次幂的判断

      return a&(a-1)?:1:0;  //返回0,则是2的若干次方

3、

There are two int variables: a and b, don’t use “if”, “? :”,“switch”or other judgement statements, find out the bigger one of the two

    参考答案:(a + b + abs(a - b)) / 2

4、

写出程序的运行结果:
#include <stdio.h>
void main()
{
int *pa = NULL;
int *pb = pa + 15;
printf("%x\n", pb);

}

结果:因为pa的长度为4 byte ,所以15 的单位为4 byte ,pb 的值为pa 加上 15 * 4 =0x3C。

5、

#include <stdio.h>
int main(void)
{
int i = 3;
int j; 
j = sizeof(++i + ++i);
printf("%d%d\n", i, j); return 0;
}

结果:输出是3和4,sizeof不是函数,是操作符,有其特殊性。sizeof后面的表达式(++i + ++i)是不会被真正执行的,sizeof的结果在编译时就确定,所以该表达式的副作用不生效,i 还是原来的值

运算符:

优先级:   ! > 算术运算符 > 关系运算符 > && > || > 赋值运算符

你可能感兴趣的:(c,null,存储,buffer,语言,编译器)