C语言细节2OK

本文详细探讨了C语言中的关键概念,包括:x是否为2的若干次幂的判断、i++与++i的区别、静态局部变量的作用、关键字static和const的功能、内联函数与宏定义的差异、数据类型在不同编译器下占字节数的变化、数组和指针的区别、预定义头文件#include的使用规则、继承与组合的区别、new/delete与malloc/free的联系与区别。此外,还讨论了进程、线程和死锁的概念,以及Linux进程间通信的方式。文章适合C语言初学者和进阶者阅读,提供了丰富的示例和解释。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

代码实现题

P28 

P32 2.5.1 

P33 2.5.3

P34 2.5.4

P35 2.6.2

P36 2.6.3 

P37 2.6.5

P38 2.6.6

P48 2.9.1

P49 2.9.2  2.9.3

P53 2.9.4


第四章

P163 6.3.4 

P165 6.3.6

第七章

1,x是否为2的若干次幂

计算形参x转化为二进制后包含1的数量:

!x&(x-1)

2,i++ 和 ++i

i++是先用值i,然后等表达式(而非语句)运算完毕,再i+1;
++i是计算i+1,然后用值i+1代入表达式。
所以
int a = 1;
int b = 2;
cout<<(a++)>=(b++)?(a++):(b++)<<endl;//3
cout<<a<<" "<<b<<endl;//2 4


P32

http://blog.youkuaiyun.com/bingxuewujian/article/details/6728396

关于本段代码在VC++6.0中的规则如下: 
1、printf函数的执行顺序是由右到左的 
2、前自增运算符(++i)先加1,再使用i,此时i已经加了1; 
3、后自增运算符(i++)先使用i,再加1,***注意这里是关键所在,VC++6.0后自增运算是要在整条语句结束以后才自加1的!
所以: 
printf("%d,%d,%d,%d,\n",i++,++i,i,i++); 
从右往左运算: 
i++得到2(i=2,后加1在整条语句执行完才进行,这里先记下) 
i还是2 (i=2,原因见上一行) 
++i得到3(i=3,先加1,后使用) 
i++得到3(i=3,后加1在整条语句执行完才进行,这里先记下) 
所以输出结果为:3,3,2,2 
然后计算刚才的两次后自增运算后,i=5 

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

3,P34

float a = 1.0f;
cout << (int)a << endl;
cout << (int&)a << endl; 
cout << boolalpha << ( (int)a == (int&)a ) << endl; // (1)输出什么?

float b = 0.0f;
cout << (int)b << endl;
cout << (int&)b << endl;
cout << boolalpha << ( (int)b == (int&)b ) << endl;// (2)输出什么?

        输出结果是:
        1
        1065353216
        false
        0
        0
        true

      

 

答:(1)(2)分别输出false和true。注意转换的应用。(int)a实际上是以浮点数a为参数构造了一个整型数,该整数的值是1,(int&)a则是告诉编译器将a当作整数看(并没有做任何实质上的转换)。因为1以整数形式存放和以浮点形式存放其内存数据是不一样的,因此两者不等。对b的两种转换意义同上,但是0的整数形式和浮点形式其内存数据是一样的,因此在这种特殊情形下,两者相等(仅仅在数值意义上)。
注意,程序的输出会显示(int&)a=1065353216,这个值是怎么来的呢?前面已经说了,1以浮点数形式存放在内存中,按ieee754规定,其内容为0x0000803F(已考虑字节反序)。这也就是a这个变量所占据的内存单元的值。当(int&)a出现时,它相当于告诉它的上下文:“把这块地址当做整数看待!不要管它原来是什么。”这样,内容0x0000803F按整数解释,其值正好就是1065353216(十进制数)。
通过查看汇编代码可以证实“(int)a相当于重新构造了一个值等于a的整型数”之说,而(int&)的作用则仅仅是表达了一个类型信息,意义在于为cout<<及==选择正确的重载版本。


PS:(int&)是什么?

其实是一个别名的强制转换

 int &A=a;
引用即是一个变量的别名,例中A是a的一个别名,每一次访问A,对A的一系列操作实际上就是访问a,对a进行操作,此时的A和a在有些编译器中时同一个地址。在定义引用时必须初始化,且必须用有内存地址的对象初始化,初始化之后不可以在指向别的对象。


4,交换两个变量swap(a,b)

a = a + b;
b = a -  b;
a = a -  b;
或
a = a ^ b;
b = a ^ b;
a = a ^ b;
或
tmp = a;
a = b;
b = tmp;
或
a^= b^=a^=b;

但是对于异或来讲,如果做成函数的话,一定要判断传入的两个数相同:

void swap(int &a, int &b){
   if(a==b);
       return ;
    a^=b^=a^=b;
}
或者还可以利用编译器表达式从左到右计算(表达式计算顺序不由C语言规定,而是由编译器决定)

a = b + 0 * (b = a);

5,关键字static的作用

1)在函数体内,一个被声明为静态的变量在这一函数被调用过程中维持其值不变(该变量存放在静态变量区)。
2)在模块内(但在函数体外),一个被声明为静态的变量/函数可以被模块内所有函数访问,但不能被模块外其它函数访问。
3)static变量默认初始化为0。

int testStatic()
{
	int x=1;
	x++;
	return x;
}
main()
{
	int i;
	for(i=0;i<5;i++)
		printf("%dn",testStatic());
}
//输出为:2 2 2 2 2 
 
int testStatic()
{
	static int x=1;
	x++;
	return x;
}
main()
{
	int i;
	for(i=0;i<5;i++)
		printf("%dn",testStatic());
}
//输出为:2 3 4 5 6


把局部变量加上static 后改变了它的存储方式即改变了它的生存期,它在文件中只被初始化一次,下一次依据上一次结果值。例如:

int f(int i)  
{  
int a=0;  
static int c=1;  
cc=c+i;  
return c;  
}  
main()  
{  
printf("%d\n",f(1));  
printf("%d\n",f(1));  
//第二次对C 初始化已经无效,c 的值为上次执行结果的值  
} //2 3

5,静态局部变量

函数体内的static静态局部变量有以下特点:
1,该变量在全局数据区分配内存;
2,静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化;
3,静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0;
4,它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束;


6,关键字const的作用

(1)可以定义 const 常量
(2)const可以修饰函数的参数、返回值,甚至函数的定义体。被const修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。

[另],

如果const位于*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;
如果const位于*的右侧,const就是修饰指针本身,即指针本身是常量。

const int a; //int型常量a
int const a; //含义同上
const int *a; //表示a是一个指针,指向一个int常量
int const* a;//含义同上
int * const a; //表示a是一个指针常量,指向一个int
int const * const a;//表示a是一个指针常量,指向一个int常量
const int* const a;//含义相同。


7,关键字const比宏定义#define的优点

1,const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而#define 只作简单的字符串替换,无类型安全检查。
2,const在编译时分配存储空间;而#define在预编译时编译,不分配存储空间。
3,有些集成化的调试工具可以对const进行调试,但不能对宏进行调试。

4,宏定义一定要程序员确保参数中不传入有副作用的表达式,如++i

8,内联函数和宏定义的区别

1. 宏不是函数,只是在编译前(编译预处理阶段)将程序中有关字符串替换成宏,而inline函数是函数,它在编译中不是单独产生代码,而是将有关代码嵌入到调用处。
2. 宏在替换的时候只是简单的文字替换,不对参数类型进行检查,而内联函数则需要进行参数类型检查。
3. 宏在编译之前进行,即先用宏体替换宏名,然后再进行编译。而内联函数显然是先编译,在执行时才调用的。


9,宏定义格式的括号

一定要外层有大括号,每个变量都有小括号

#define  SWAP(A,B) ((A)>=(B)?(A):(B))

10,C语言数据类型占字节数

因为我问了他是否是在32位平台下(细节决定成败)。

16位编译器
char :1个字节
char*(即指针变量): 2个字节
short int : 2个字节
int:  2个字节
unsigned int : 2个字节
float:  4个字节
double:   8个字节
long:   4个字节
long long:  8个字节
unsigned long:  4个字节


32位编译器
char :1个字节
char*(即指针变量): 4个字节(32位的寻址空间是2^32, 即32个bit,也就是4个字节。同理64位编译器)

short int : 2个字节
int:  4个字节
unsigned int : 4个字节
float:  4个字节
double:   8个字节
long:   4个字节
long long:  8个字节
unsigned long:  4个字节


64位编译器
char :1个字节
char*(即指针变量): 8个字节
short int : 2个字节
int:  4个字节
unsigned int : 4个字节
float:  4个字节
double:   8个字节
long:   8个字节
long long:  8个字节
unsigned long:  8个字节

11,数组和指针的区别

例如:

int a [10];
int * pa;

char c[10];
char * pc;
1,指针是一个变量,因此,在C语言中,语句pa=a和pa++都是合法的。但数组名不是变量而是常量,因此,类似于a=pa和a++形式的语句是非法的。
2,当数组名传递给一个函数时,实际上传递的是该数组第一个元素的地址。即形参 char s[];传入时自动转化为char *s;

void f(char str[100])
{
	sizeof(str);//4 因为形参是数组,会默认转为指针:char * str 所以为4,而非数组100*4
}

当数组名作为函数形参时,在函数体内,该数组自动退化为同类型的指针;(因此函数无法传递数组结构,只能传递数组退化的指针可以利用该特性辨析strlen函数的另一个版本,该函数用于计算一个字符串的长度。

// strlen: return length of string s
int strlen(char *s)
{
	int n;
	for(n=0; *s!='\0';s++)
		n++;
	return n;
}
因为s是一个指针,所以对其执行自增运算是合法的。执行s++运算不会影响到strlen函数的调用者中的字符串,它仅对该指针在strlen函数中的私有副本进行自增运算。因此,类似于下面这样的函数调用:
strlen("hello world");                               // string constant
strlen(array);                                       // char array[100]
strlen(ptr);                                         // char *ptr

3,修改内容上的区别

char a[] = "Hello";
a[0] = 'A';

char *p = "Hello";//p指向常量字符串
p[0] = 'A';//运行时出错

12,数组名的含义

1,针对 &a 和 &c,只有把a和c当做变量才能有&操作,所以这里都看做数组,a为int型数组,c为字符数组。

2,而a和c本身的值却有不同的定义,a的值定义为a[0]的地址;c的值定义为字符串

3,&a[0]和&c[0]都是计算得来的,&a[0]==&(*(a+0))==a;&c[0]==&(*(c+0))==c。所以看到a[0]这种数组符号第一反应先翻译为*(a+0);

	//-------------------------------------------------------
	int a[5] = {1,2,3,4,5};

	//把a看做一个变量,才可以&操作,这里a为一个数组
	&a;       //0x0018FF34  数组a地址
	(&a)+1;   //0x0018FF48  数组a整个结束后的首地址
	sizeof(a);//20          代表的是整个数组a大小

	a;        //0x0018FF34  数组元素a[0]地址,[数组的值被处理为a[0]的地址]
	a+1;      //0x0018FF38  数组元素a[1]地址
	&a[0];    //0x0018FF34  数组元素a[0]地址,因为&a[0]=&(*(a+0))=a
	&a[0]+1;  //0x0018FF38  数组元素a[1]地址
	sizeof(a[0]);//4        代表数组元素a[0]大小
    

	//-------------------------------------------------------
	char c[5] = "good";

	&c;       //0x0018FF2C  字符数组c的地址
 	(&c)+1;   //0x0018FF31  字符数组c整个结束后的首地址
	sizeof(c);//5           代表的是整个字符数组c的大小

	c;        //good,[字符数组的值处理为字符串]  
	c+1;      //ood
	&c[0];    //计算==c,所以为good
	&c[0]+1;  //计算==c+1,所以为ood
	sizeof(c[0]);//1   


13,预定义头文件<>和" "的区别

< >是从系统默认的路径寻找头文件,比如编译器的库和头文件安装目录。
" "是先从你的代码文件所在的目录寻找头文件,如果找不到再从系统目录里找。


14,C和C++中的struct的区别;C++中struct和class的区别

C中的struct只能是一些变量的集合体,可以封装数据却不可以隐藏数据,而且成员不可以是函数,而C++中可以。

C++中,struct和class的主要区别在于默认的存取权限不同,struct默认为public,而class默认为private。另,c++中class可以有继承,虚函数,多态,而c++中struct不可以。

16,sizeof的用法

1,指针的sizeof操作
指针均可看为变量类型的一种。所有指针变量的sizeof 操作结果均为4。  

注意:
double *p; 
sizeof(p);//4;
sizeof(*p);//8相当于sizeof(double);  

2,静态数组的sizeof操作  
对于静态数组,sizeof可直接计算数组大小;

例:
int a[10];
char b[]="hello";
sizeof(a);//等于4*10=40;
sizeof(b);//等于6;
 注意:数组做型参时,数组名称当作指针使用!!
void  fun(char p[])
{
	sizeof(p);//等于4
} 

3,经典问题: 
double* (*a)[3][6]; 
cout<<sizeof(a)<<endl; // 4 a为指针
cout<<sizeof(*a)<<endl; // 72 *a为一个有3*6个指针元素的数组
cout<<sizeof(**a)<<endl; // 24 **a为数组一维的6个指针
cout<<sizeof(***a)<<endl; // 4 ***a为一维的第一个指针
cout<<sizeof(****a)<<endl; // 8 ****a为一个double变量
问题解析:a是一个很奇怪的定义,他表示一个指向double*[3][6]类型数组的指针。既然是指针,所以sizeof(a)就是4。 
      既然a是执行double*[3][6]类型的指针,*a就表示一个double*[3][6]的多维数组类型,因此sizeof(*a)=3*6*sizeof(double*)=72。同样的,**a表示一个double*[6]类型的数组,所以sizeof(**a)=6*sizeof  (double*)=24。***a就表示其中的一个元素,也就是double*了,所以sizeof(***a)=4。至于****a,就是一个double了,所以sizeof(****a)=sizeof(double)=8。    

4,格式的写法
   sizeof操作符,对变量或对象可以不加括号,但若是类型,须加括号。
5,使用sizeof时string的注意事项
   string s="hello";
   sizeof(s)等于string类的大小,sizeof(s.c_str())得到的是与字符串长度。


17,指针与引用的区别

1、指针是一个地址值,而引用是一个实体的别名。

这句干巴巴的话我理解了很久,终于有一点点明白这两者究竟不同在哪里。指针,是一种数据类型,它的值是一个地址,当声明一个指针,编译器会为这个指针变量分配内存空间;而引用不是数据类型,引用本身不会占内存空间,编译器也不会为引用分配空间。支持这一点的最有力的证据是,对一个指针变量取地址,会得到一个与这个指针本身的值不同的地址值,而对一个引用做取址运算,得到的地址则是引用的目标变量的地址。
2、指针可以指向不同的地址空间,但是引用一旦定义,只能指向那个固定的实体。
3、指针可以在定义的时候初始化,也可以定义为NULL值,也可以在定义的时候不初始化,过后再指定它的值,而引用必须在定义的时候初始化,而且必须用某个实体对其进行初始化,一旦定义完成,其值不可更改。
4、在传参的时候,使用指针传参,编译器需要给指针另行分配存储单元,存储一个该指针的副本,在函数中对这个副本进行操作;使用引用传参,编译器就不需要分配存储空间和保存副本了,函数将直接对实参进行操作。所以使用引用使得程序的效率更高。

PS:指针解引用有代价,引用解引用无代价。

例如:

#include <stdio.h> 
#include <stdlib.h> 
void getmemory(char *p)  
{  
	p=(char *) malloc(100);  
	strcpy(p,"hello world");  
}  
int main( )  
{  
	char *str=NULL;  
	getmemory(str);  
	printf("%s/n",str);  
	free(str);  
	return 0;  
} 
本题将导致程序崩溃,因为getmemory 函数中的malloc 是不能返回动态内存的,free()函数对str 操作也很危险。
解析:

/这是原来的函数,使用的是值传递方式
void GetMemory(char *p){ p=(char *)malloc(100); } 
//这是正确的内存分配函数,使用的是双重指针,传递进来的是str的指针地址,*p就是原来的str
void GetMemory2(char **p) { *p=(char*)malloc(100); }
//这是使用引用的内存分配函数
void GetMemory3(char* &p) { p=(char*)malloc(100); }
void Test(void)
{ 
	char *str=NULL; 
	GetMemory(str); // 该函数做的工作是 p=str, p=malloc(100); 和str一点关系都没有,str=NULL;
	GetMemory2(&str); //该函数做的工作是 char**p=&str, str=*p=malloc(100),内存分配成功
	GetMemory3(str); // 传递的是str的引用,函数内的 p还是str,分配成功
	strcpy(str, "Hello World!"); //现在可以 知道,第一个函数分配内存错误,strcpy也会失败
	printf("%s\n", str); //输出只能是空
}

18,C++中的this指针

在C++程序中同一个类的不同对象除数据成员占不同的内存空间外,成员函数代码段是共用的。那么当不同对象的成员函数引用数据成员时,怎么能保证引用的是所指定的对象的数据成员呢?因此为了保证引用的准确性,在每一个成员函数中都包含了一个特殊的指针,这个指针的名字是固定的,称为this。它是指向本类对象的指针,它的值是当前被调用的成员函数所在的对象的起始地址。如以下简单例子:

class Time
{
  public:
	  void showtime();
	  {
		  cout<<hour<<":"<<min<<":"<<sec<<endl;
      }
  private:
      int hour;
      int min;
      int sec;
};

int main(int argc, char *argv[]) 
{ 
	Time t;
	t.showtime();
	return 0;
}
会被编译为:

class Time
{
  public:
	  void showtime(Time *this);//编译系统自动实现
	  {
		  cout<<this->hour<<":"<<this->min<<":"<<this->sec<<endl;//编译系统自动实现
      }
  private:
      int hour;
      int min;
      int sec;
};

int main(int argc, char *argv[]) 
{ 
	Time t;
	t.showtime(&t);//编译系统自动实现
	return 0;
}

19,重载(overload),重写(override),隐藏(hide)

重载overload,这个概念是大家熟知的。在 同一可访问区内被声名的几个【 函数名相同参数列不同参数的类型、个数、顺序不同), 返回类型无关】,程序会根据不同的参数列来确定具体调用哪个函数,这种机制就是重载。重载 不关心函数的返回值类型,即返回类型不同无法构成重载。此外,C++ 中的const成员函数也可以构成overload。
    总结一下重载的特征:
  1、一个类中:处在相同的空间中,即相同的范围内;
  2、同名:函数名相同;
  3、参数不同:即参数个数不同,或相同位置的参数类型不同;
  4、const不影响重载:const成员函数可以和非const成员函数形成重载;
        5、virtual关键字、返回类型对是否够成重载无任何影响。


覆盖override,是指派生类中存在重新定义的函数,其【 函数名相同、参数列相同、返回类型相同】必须同父类中的相对应被覆盖的函数严格一致,覆盖函数和被覆盖函数只有函数体(花括号中的部分)不同,当派生类对象调用子类中该同名函数时会自动调用子类中的覆盖版本,而不是父类中的被覆盖函数版本,这种机制就叫做覆盖,特征是:       
        1、不同的范围(分别位于派生类与基类);       
        2、函数名字相同;       
        3、参数相同;       
        4、基类函数必须 有virtual关键字。  


隐藏hide。所谓的隐藏,指的是派生类类型的对象、指针、引用访问基类和派生类都有的同名函数时,访问的是派生类的函数,即隐藏了基类的同名函数。隐藏规则的底层原因其实是C++的名字解析过程。在继承机制下,派生类的类域被嵌套在基类的类域中。派生类的名字解析过程如下:
  1、首先在派生类类域中查找该名字。
  2、如果第一步中没有成功查找到该名字,即在派生类的类域中无法对该名字进行解析,则编译器在外围基类类域对查找该名字的定义。
    总结一下隐藏的特征:
        1、如果派生类的函数与基类的【 函数名相同,参数列不同,基类无关virtual关键字】。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。       

        2、如果派生类的函数与基类的【函数名相同,参数列相同,但是基类函数没有virtual关键字】。此时,基类的函数被隐藏(注意别与覆盖混淆)。 

20,继承和组合的区别

1,我的理解

继承:父与子的关系。is-a关系。
组合:整体与部分的关系。has-a关系。有点像设计模式中的代理模式。
针对has-a的逻辑关系,使用private继承相当于private组合(两种方式实现一种功能)。当然用private组合的方式更好,因为这样更节省空间,这里的空间代价主要指的是继承的时候额外占用的空间补全。

2,面向对象系统中功能复用的两种最常用技术是类继承对象组合(object composition)

类继承允许你根据其他类的实现来定义一个类的实现。这种通过生成子类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方
式中,父类的内部细节对子类可见。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。

3,继承和组合各有优缺点。

类继承是在编译时刻静态定义的,且可直接使用,因为程序设计语言直接支持类继承。类继承可以较方便地改变被复用的实现。当一个子类重定义一些而不是全部操作时,它也能影响它所继承的操作,只要在这些操作中调用了被重定义的操作。但是类继承也有一些不足之处。首先,因为继承在编译时刻就定义了,所以无法在运行时刻改变从父类继承的实现。更糟的是,父类通常至少定义了部分子类的具体表示。因为继承对子类揭示了其父类的实现细节,所以继承常被认为“破坏了封装性” [ S n y 8 6 ]。子类中的实现与它的父类有如此紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化。当你需要复用子类时,实现上的依赖性就会产生一些问题。如果继承下来的实现不适合解决新的问题,则父类必须重写或被其他更适合的类替换。这种依赖关系限制了灵活性并最终限制了复用性。一个可用的解决方法就是只继承抽象类,因为抽象类通常提供较少的实现。

对象组合是通过获得对其他对象的引用而在运行时刻动态定义的。组合要求对象遵守彼此的接口约定,进而要求更仔细地定义接口,而这些接口并不妨碍你将一个对象和其他对象一起使用。这还会产生良好的结果:因为对象只能通过接口访问,所以我们并不破坏封装性;只要类型一致,运行时刻还可以用一个对象来替代另一个对象;更进一步,因为对象的实现是基于接口写的,所以实现上存在较少的依赖关系。对象组合对系统设计还有另一个作用,即优先使用对象组合有助于你保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物。另一方面,基于对象组合的设计会有更多的对象(而有较少的类),且系统的行为将依赖于对象间的关系而不是被定义在某个类中。这导出了我们的面向对象设计的第二个原则:优先使用对象组合,而不是类继承

21,new/delete与malloc/free的联系与区别

malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。

对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。

因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。

如果用free释放“new创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。如果用delete释放“malloc申请的动态内存”,理论上讲程序不会出错,但是该程序的可读性很差。所以new/delete必须配对使用,malloc/free也一样。

22,main函数执行之[前/后][会/可以]做什么

main函数执行之前,主要就是初始化系统相关资源:
1.设置栈指针
2.初始化static静态和global全局变量,即data段的内容
3.将未初始化部分的赋初值:数值型short,int,long等为0,bool为FALSE,指针为NULL,等等,即.bss段的内容
4.运行全局构造器,估计是C++中构造函数之类的吧
5.将main函数的参数,argc,argv等传递给main函数,然后才真正运行main函数


问题1:如何在main函数前打log?

如果是c++的话,可以搞一个全局对象,把你要执行的方法写到这个对象的构造函数里面。这个方法就可以在main之前执行了。

static A a;//其中A的构造函数中打log

pubic static void main()
{
}

问题2:如何在main函数之后调用函数?

可以用_onexit注册一个函数:

#include <iostream>
#include <cstdlib>
using namespace std;
int func1();
//_onexit 使用方法   函数必须是 带有int类型返回值的无参数函数
//_onexit 包含在cstdlib中  原始是c语言中的库函数
int main(int argc,char * argv[])
{
	_onexit(func1);//无论函数放到main中哪个位置都是最后执行
	cout<<"this is the first one"<<endl;
	
	cout<<"this"<<endl;
}

int func1()
{
	cout<<"this executed at last time"<<endl;
	return 0;
}

另外一种实现:

在gcc中,可以使用attribute关键字,声明constructor和destructor,代码如下:

#include <stdio.h>  
 
__attribute((constructor)) void before_main() 
{ 
    printf("%s/n",__FUNCTION__); 
} 
 
__attribute((destructor)) void after_main() 
{ 
    printf("%s/n",__FUNCTION__); 
} 
 
int main( int argc, char ** argv ) 
{ 
    printf("%s/n",__FUNCTION__); 
    return 0; 
} 

23,堆栈溢出的一般原因

答 :1.没有回收垃圾资源
         2.层次太深的递归调用


24,静态成员函数能不能同时也是虚函数

答:不能。

用static声明的函数是静态函数。静态函数可以分为全局静态函数和类的静态函数成员。静态成员函数都没有this指针,因为它不需要实例。但调用虚函数需要从一个实例中指向虚函数表的指针以得到函数的地址,因此,调用虚函数需要一个实例。两者互为矛盾。


25,程序、进程与线程的区别

定义:
程序只是一组指令的有序集合,
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是系统进行资源分配和调度的一个独立单位
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),一个线程可以创建和撤销另一个线程;

一 进程与线程区别与联系
(1)    划分尺度:线程更小,所以多线程程序并发性更高;
(2)    资源分配:进程是资源分配的基本单位,同一进程内多个线程共享其资源;
(3)    地址空间:进程拥有独立的地址空间,同一进程内多个线程共享其资源;
(4)    处理器调度:线程是处理器调度的基本单位;
(5)    执行:每个线程都有一个程序运行的入口,顺序执行序列和程序的出口,但线程不能单独执行,必须组成进程,一个进程至少有一个主线程。简而言之,一个程序至少有一个进程,一个进程至少有一个线程.


二 进程和程序区别和联系
(1)程序只是一组指令的有序集合,它本身没有任何运行的含义,它只是一个静态的实体。而进程则不同,它是程序在某个数据集上的执行。进程是一个动态的实体,它有自己的生命周期。反映了一个程序在一定的数据集上运行的全部动态过程。
(2)进程和程序并不是一一对应的,一个程序执行在不同的数据集上就成为不同的进程,可以用进程控制块来唯一地标识每个进程。而这一点正是程序无法做到的,由于程序没有和数据产生直接的联系,既使是执行不同的数据的程序,他们的指令的集合依然是一样的,所以无法唯一地标识出这些运行于不同数据集上的程序。一般来说,一个进程肯定有一个与之对应的程序,而且只有一个。而一个程序有可能没有与之对应的进程(因为它没有执行),也有可能有多个进程与之对应(运行在几个不同的数据集上)。
(3)进程还具有并发性和交往性,这也与程序的封闭性不同。


26,Linux进程间通信(IPC)方式

进程间通信(IPC)Inter-Process Communication

1,管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
2,有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
3,信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
4,消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
5,信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
6,共享内存( shared memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
7,套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。


信号量:一种锁机制。可以解决进程之间的同步与互斥问题。

共享内存:优点是快速,实现简单。缺点是没有同步机制。所以必须依靠某种同步机制,如互斥锁和信号量。


linux进程间通信: 管道、消息队列、共享内存

ipcs:检查系统上共享内存的分配
ipcrm:手动解除系统上共享内存的分配

27,死锁的必要条件

死锁定义:操作系统中的死锁被定义为系统中两个或者多个进程无限期地等待永远不会发生的条件,系统处于停滞状态,这就是死锁。
死锁原因
(1) 因为系统资源不足。
(2) 进程运行推进的顺序不合适。
(3) 资源分配不当等。
产生死锁的四个必要条件
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件(占有等待):一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件(不可抢占):进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。


28,进程fork函数

fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
    1)在父进程中,fork返回新创建子进程的进程ID;
    2)在子进程中,fork返回0;
    3)如果出现错误,fork返回一个负值;


例题1:计算下面代码理论上总共打印了多少行:(网易2011笔试题)

http://www.spongeliu.com/123.html

#include
int main(){
        int i;
        for(i = 0; i<5; i++){
                fork();
                printf("%d\n",getpid());
                fflush(stdout);
        }
}
这里不只是最终节点,每个节点(包括中间节点)都要输出,所以是2+4+8+16+32=62。

例题2:问下面的代码执行后总共产生了多少进程(不包括主进程)?(2009 EMC笔试)

#include
int main(){
        fork();
        fork() && fork() || fork();
        fork();
}
对于多个fork,实际上最终进程数都会*2计算。

所以这里只需要看第二行fork() && fork() || fork();会产生多少个进程。这里是5个(画图数出来),所以结果是5*2*2-1=19。


例题3:该程序共生成多少个子进程?
http://blog.youkuaiyun.com/hikaliv/article/details/4276758

for( i = 0; i < 5; i++ ) 
    if( fork() == 0 ) 
        continue; 

if语句的continue是假的!实际上等价于:

fork();
fork();
fork();
fork();
fork();
所以是2^5-1=31。


29,typedef struct和struct的区别

C语言中:

29.1 定义结构变量的一般格式为:

struct 结构名
{
	类型 变量名;
	类型 变量名;
	...
} 结构变量;

使用方法:

//定义一个结构STUDENT,并同时声明一个变量stu1
struct STUDENT
{
	int  a;
	int  b;
} stu1;

//再声明一个变量stu2
struct STUDENT stu2;

29.2 另一种常用格式为:
typedef struct 结构名
{
	类型 变量名;
	类型 变量名;
	...
} 结构别名;
这样声明变量时就不用写“struct 结构名 结构变量;”,typedef将struct 结构名{}定义为结构别名,因此使用“结构别名 结构便量;”即可。

使用方法:

typedef struct STUDENT
{
	int a;
	int b;
} STU;
//声明一个结构变量
STU stu1;
//用另一种方法声明变量
struct STUDENT stu2;

这里的STU实际上就是 struct STUDENT的别名。

当然也可以将STUDENT这个结构名省略:

typedef struct
{
	int a;
	int b;
} STU;
//声明一个结构变量
STU stu1;
//不能这样了struct STUDENT stu2;

C++中:

struct Student
{
	int a;
};
于是就定义了结构体类型Student,声明变量时直接Student stu2;
另外注意: 在C中,struct不能包含函数。在C++中,对struct进行了扩展,可以包含函数。

http://www.cnblogs.com/qytan36/archive/2010/05/25/1743838.html

30,命令行编译C或C++文件

编译C文件:

最简单的是:gcc hello.c,默认的情况下将生成a.out的可执行性文件,你只需要在终端上输入./a.out就可以看到执行的结果.

gcc -o hello hello.c
编译C++文件:

g++ -o hello hello.c

31,stl::map内部实现

增、删、查都是O(log(N))。
不过一般现在流行的STL中都不使用平衡二叉树,而是使用红黑树,是一种不完全平衡的二分查找树。增删查也都是O(log(N)),不过增删操作比平衡二叉树要快得多,但查找稍慢(因为不完全平衡)。

32,linux查看cpu内存硬盘等等与系统性能调试相关的命令

1,CPU 
uptime
sar

2,内存 
free
vmstat

3,磁盘I/O带宽 
iostat

4,网络I/O带宽

netstat

tcpdump:主要是截获通过本机网络接口的数据,用以分析。能够截获当前所有通过本机网卡的数据包。它拥有灵活的过滤机制,可以确保得到想要的数据。

5,进程间通信

ipcs:检查系统上共享内存的分配
ipcrm:手动解除系统上共享内存的分配

35,计算机书籍

编程语言:《C++ Primer》 《C++沉思录》《Thinking in Java》《C专家编程》《Effective C++》《C陷阱与缺陷》

Unix/Linux:《Unix编程艺术》《鸟哥的Linux私房菜》《Linux设计思想》

体系结构:《深入理解计算机系统》

算法书籍:《算法导论》《编程之美》《编程珠玑》

衍生书籍:《黑客与画家》《编码的奥秘》《程序员修炼之道》《重构》《大话设计模式》《人月神话》

38,智能指针

Boost函数库

39,C函数库中strcpy,strncpy,memcpy,memmove

http://blog.youkuaiyun.com/piaojun_pj/article/details/5945926

1,strcpy:【遇到'\0'结束】

char *strcpy(char *dest, const char *src) {
	assert(dest!=NULL&&src!=NULL);
	char * tmp = dest;
	while((*dest++=*src++)!='\0')
		;
	return tmp;
}

功能:进行字符串拷贝,遇到‘\0’后结束拷贝,要求原串地址与目的串地址不能重合

2,strncpy:【遇到'\0'结束】【内存个数size】

har *strncpy(char * dest, const char * src, int count){
	assert(dest!=NULL&&src!=NULL&&count>=0);
	char * tmp = dest;
	while(count--&&(*dest++=*src++)!='\0')
		;
	return tmp;
}

功能:依然是字符串的拷贝,但是拷贝固定的字符个数,也需要受到\0的限制

3,memcpy:【内存个数size】

void * memcpy(void * dest, const void *src, int count){
	assert(dest!=NULL&&src!=NULL);
	char * tmp = dest;
	while(count--){
		*dest++=*src++;
	}
	return tmp;
}

功能:memcpy()用来拷贝src所指的内存内容前n个字节到dest所指的内存地址上。与strcpy()不同的是,memcpy()会完整的复制n个字节,不会因为遇到字符串结束'\0'而结束。

4, memmove:【内存个数size】【解决重叠拷贝】

void *memmove(void *s, const void *ct, int count)
与memcpy类似,所不同的是,当对象重叠时,该函数仍能正确执行
5,memset:【初始化

void * memset(void* buffer, int c, int count){
	assert(buffer!=NULL);
	char * tmp = buffer;
	while(count--){
		*buffer++=(char)c;
	}
	return tmp;
}

40,滑动窗口协议

http://baike.baidu.com/view/1341477.htm 

动画演示:

http://kjmt.5any.com/hlwjqyy/coursebase/03/0301/content/ch03/index030303.htm


41,设计模式之工厂模式【UML】

http://blog.youkuaiyun.com/hguisu/article/details/7505909


42,malloc和new的区别

1,malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。

2,new 建立的是一个对象,malloc分配的是一块内存。
3,对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。 因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。 new 不止是分配内存,而且会调用类的构造函数,同理delete会调用类的析构函数,而malloc则只分配内存,不会进行初始化类成员的工作,同样free也不会调用析构函数。

45,STL模板vector能不能放引用??

不能。

问题:首先引用一般的理解就是源对象本身,所以一个函数f(int &a)其实就是将直接使用a(而不是拷贝了a,也不是传入了地址)

原因:存储在vector中的元素之间必须能够复制和赋值,引用类型不能相互“一般意义下的赋值”。

46,STL模板vector能不能放指针??

不能。

原因:

47,STL模板vector的push_back内部实现??

xxx

48,设计模式之单例模式

使用方法:
Singleton obj = Singleton.getInstance();

第一种实现方法:
/** 
 * 实现单例访问Singleton的第1次尝试 
 */  
public class Singleton {  
   
    //私有,静态的类自身实例
    private static Singleton instance = new Singleton();
 
    //私有的构造子(构造器,构造函数,构造方法)
    private Singleton(){};
   
    //公开,静态的工厂方法
    public static Singleton getInstance() {
        return instance;
    }
}  
缺点:
这个单例类在自身被加载时instance会被实例化,即便加载器是静态的。因此,对于资源密集,配置开销较大的单体更合理的做法是将实例化(new)推迟到使用它的时候。即惰性加载(Lazy loading),它常用于那些必须加载大量数据的单体。

第二种实现方法:
/** 
 * 实现单例访问Singleton的第2次尝试 
 */  
public class Singleton {  
   
    //初始为null,暂不实例化
    private static Singleton instance = null;  
 
    //私有的构造子(构造器,构造函数,构造方法)
    private Singleton(){};
   
    public synchronized static Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
}  
用C或者C++实现:
可以在getInstance的时候进行lock。

49,select和epoll的区别

select跟epoll都能提供多路I/O复用的解决方案。
1,select函数
select的监控信息在函数调用中由(fd_set *readfds, fd_set *writefds, fd_set *exceptfds)传入和传出。
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

2,epoll函数
epoll_wait函数中看不到相关的监控信息,因为是通过epoll_ctl已经加入
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

select和epoll都是用来解决IO复用问题,都是非阻塞的。

50,什么是RPC

RPC,就是将数据序列化之后传递给接收的服务器,然后获取一个序列化后的返回值
在网络通信中,不管是TCP 还是UDP,我们都要在传输层上设计自己的应用层协议,使得前后端的数据可以相互通信传输一个可以识别的内容。
后来,人们期望能够更方便一点地让前后端进行交互,于是提出了RPC,就像调用函数一样来让前后端来进行通信,屏蔽掉复杂的应用层协议。


51,什么是回调函数

回调函数是一个通过函数指针调用的函数。如果你把函数指针(函数的入口地址)传递给另一个函数,当这个函数指针被用来调用它所指向的函数时,我们就说这个函数是回调函数。

Node * Search_List (Node * node, const int value)
{
  while (node != NULL)
	{
   	if (node -> value == value)
      {
			break;
      }
      node = node -> next;
   }
   return node;
}
这个函数看上去很简单,但是我们考虑一个问题:它只能适用于值为整数的链表,如果查找一个字符串链表,我们不得不再写一个函数。

NODE *Search_List(NODE *node, int (*compare)(void const *, void const *) , void const *desired_value);
{
  while (node != NULL)
  {
      if (compare((node->value_address), desired_value) == 0)
      {
          break;
      }
      node = node->next;
  }
   return node;
}

52,数据库引擎

MyISAM是MySQL的默认存储引擎,基于传统的ISAM类型,表级锁定
InnoDB是事务型引擎,支持行级锁定

53,数据库索引

问题:为什么数据库中不能用哈希表做索引?

解答:因为哈希表虽然快速简单,但是其原理导致在逻辑上相邻的元素在物理上不相邻(非聚簇索引),所以无法支持简单的where id>1 类似的操作。

53,linux的文件缓冲

C标准库的I/O缓冲区有三种类型:全缓冲、行缓冲和无缓冲。

53.1 全缓冲

如果缓冲区写满了就写回内核。常规文件通常是全缓冲的。

C标准库为每个打开的文件分配一个I/O缓冲区以加速读写操作,通过文件的FILE结构体可以找到这个缓冲区,用户调用读写函数大多数时候都在I/O缓冲区中读写,只有少数时候需要把读写请求传给内核。以fgetc/fputc为例,当用户程序第一次调用fgetc读一个字节时,fgetc函数可能通过系统调用进入内核读1K字节到I/O缓冲区中,然后返回I/O缓冲区中的第一个字节给用户,把读写位置指向I/O缓冲区中的第二个字符,以后用户再调fgetc,就直接从I/O缓冲区中读取,而不需要进内核了,当用户把这1K字节都读完之后,再次调用fgetc时,fgetc函数会再次进入内核读1K字节到I/O缓冲区中。另一方面,用户程序调用fputc通常只是写到I/O缓冲区中,这样fputc函数可以很快地返回,如果I/O缓冲区写满了,fputc就通过系统调用把I/O缓冲区中的数据传给内核,内核最终把数据写回磁盘。有时候用户程序希望把I/O缓冲区中的数据立刻传给内核,让内核写回设备,这称为Flush 操作,对应的库函数是fflush,fclose函数在关闭文件之前也会做Flush操作。

53.2 行缓冲

如果用户程序写的数据中有换行符就把这一行写回内核,或者如果缓冲区写满了就写回内核。标准输入和标准输出对应终端设备时通常是行缓冲的。

53.3 无缓冲

用户程序每次调库函数做写操作都要通过系统调用写回内核。标准错误输出通常是无缓冲的,这样用户程序产生的错误信息可以尽快输出到设备。

53.4 关于printf的IO操作时间:

在测定函数时间的时候,遇到了这个问题,其实printf不管打到屏幕,还是打到文件都是很耗时的IO操作时间,会影响整个数据测试的时间。
这里理一下对这几个操作的耗时的理解:
A,输出打到屏幕
屏幕也是IO设备,差别在于屏幕是行缓冲,所以每次打一行都会调用一次IO,所以非常耗时。
B,输出打到文件
文件输出是IO操作,差别在于,有BUFF,不是行缓冲,所以在buff内(内存区域)存满后一次打到硬盘,时间尚可。
C,输出打到/dev/null
也是IO设备,但比较特殊,猜测其实现应该是一旦检测要打开/dev/null文件,其内部的写是个空循环。

整理:屏幕,文件,/dev/null都是物理IO设备,但是在实际的具体的写write函数实现不同。有的是行缓冲,有的是整个BUFF,有的根本就不写。



字符串匹配算法

如何实现一个hash table
堆排序
实现 全排列 Amn  和 Cmn


一个字符串 循环

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值