程序员面试宝典 - 总结
1 x=x+1,x+=1,x++,哪个效率最高?为什么?
解析:
x=x+1最低,因为它的执行过程如下:
1)读取右x的地址。
2)x+1
3)读取左x的地址
4)将右值传给左边的x(编译器并不左右x的地址相同)。
x+=1其次,其执行过程如下:
1)读取右x的地址
2)x+1
3)将得到的值传给x(因为x的地址已经读出)。
x++,效率最高,其执行过程如下:
1)读取右x的地址
2)x自增1.
2 i++和++i的区别
i=3;
则j=i++*i++; 则j等于9,此时i累积为5
而k=++i*++i; 则k等于49,先累加i为7
3 在条件表达式中'A'==a与a==’A‘哪一种写法好。
第一种写法'A'==a比较好一些。这时如果把"=="误写做"="的话,因为编译器不允许对常量赋值,就可以检查到错误。
4 当表达式中存在有符号类型和无符号类型时。所有的操作数都自动转换为无符号类型。
5 使用extern "C"的理由
- define宏是在预定义阶段,const常量在编译运行阶段使用
- const定义的只读变量是有数据类型的,编译器可以对其进行数据类型检查,而#define宏定义的常量只是进行简单的字符替换,没有类型检查,且有时还会产生意料不到的错误(边际效应)。
- 有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。
- 当定义局部变量时,const作用域仅限于定义局部变量的函数体内。但用#define时其作用域不仅限于定义局部变量的函数体内,而是从定义点到整个程序的结束点。但也可以用#undef取消其定义从而限定其作用域范围。
另外,
C++中支持#define是因为要兼容C,C++中不提倡用#define来定义常量。
#define也可以定义带参数的宏,C++中使用inline函数来代替。
扩展知识:
C中的const的意思是“一个不能被改变的普遍变量”。在C中,它总是占用内存,而且它的名字是全局符。C编译器不能把const看成一个编译期间的常量。在C中,如果写:
const bufsize=100;
char buf[bufsize];
尽管看起来好像做了一件合理的事,但这将得到一个错误的结果。因为bufsize占用内存的某个地方,所以C编译器不知道它在编译时的值。在C语言中可以选择这样书写:
const bufsize;
这样写在C++中是不对的,而C编译器则把它作为一个声明,这个声明指明在别的地方有内存分配。因为C默认const是外部连接的,C++默认const是内部连接的,这样,如果在C++中想完成与C中同样的事情,必须用extern把内部连接改成外部连接:
extern const bufsize; //declaration only
这种方法也可以在C语言中。在C语言中使用限定符const不是很有用,即使是在常数表达式里(必须在编译期间被求出)想使用一个已命名的值,使用const也不是很有用的。C迫使程序员在预处理器里使用#define。
11 内联函数和宏定义
宏和内联函数的区别:
- 宏是简单的字符串替换,没有参数类型检查,内联函数要做参数类型检查
- 宏在编译之前进行(编译预处理阶段),即先用宏替换宏名,然后再编译。函数是编译之后,在执行调用的。所以宏占用的是编译的时间,函数占用的是执行的时间。
- 宏的参数不占内存空间,因为只是简单的字符串替换。函数的形参会占内存空间
- 函数要有调用的开销,宏没有。
指针和引用
1 指针和引用的差别
1)非空区别。在任何情况下都不能使用指向空值的引用。一个引用必须总是指向某些对象。因此如果你使用一个变量并让它指向一个对象,但是该变量在某些时候也可能不指向任何对象,这时你应该把变量声明为指针,因为这样你可以赋空值给该变量。相反,如果变量肯定指向一个对象,例如你的设计不允许变量为空,这时你就可以把变量声明为引用。不存在指向空值的引用这个事实意味着使用引用的代码效率比使用指针要高。
2)合法性区别。在使用引用之前不需要测试它的合法性。相反,指针则应该总是被测试,防止其为空。
3)可修改区别。指针和引用的另一个重要的不同是指针可以被重新赋值以指向另一个不同的对象。但是引用则总是指向在初始化时被指定的对象,以后不能改变,但是指定的对象其内容可以改变。
4)应用区别。总的来说,在以下情况下你应该使用指针:一是你考虑到存在不指向任何对象的可能,二是你需要能够在不同的时刻指向不同的对象。如果总是指向一个对象并且一旦指向一个对象后就不会改变指向,那么你应该使用引用。
char c[] = “hello”; char *c = “hello”;的区别:
Notes: 前者是分配一个局部数组,位于内存的栈中;后者是分配一个全局数组,位于内存的全局数据区。全局数据区的值不可更改,局部区的数据可改变:
char c[] = "hello";cout<<c<<endl; //hello
c[0] = 'H';cout<<c<<endl; //Hello
char *c1 = "leo";cout<<c1<<endl;//leo
c1 = "jia";cout<<c1<<endl;//jia,指针值改变
//*c1 = 'J'; //false,内存不可写
//c1[0] = 'J';//false,内存不可写
指针和句柄
(1)指针和句柄的区别联系
指针标记物理内存地址,而句柄是指向指针的指针,windows用句柄来标记系统资源,隐藏系统信息。它是windows在内存中维护的一个对象的内存物理地址列表的整数索引,是一个uint。对象的物理地址是变化的,程序将想访问的对象的句柄传递给系统,系统根据句柄检索自己维护的对象列表来访问对象。
Windows是一个以虚拟内存为基础的操作系统,其内存管理器经常在内存中移动对象来满足不同程序的内存需要,为了能继续访问被移动后的对象,系统把对象新的地址用句柄保存起来,用句柄可以间接知道对象的具体物理地址。句柄地址(稳定)->对象在内存中的地址(不稳定)->实际对象。
C专家编程 总结
1 类型转换
当执行算术运算时,操作数的类型如果不同,就会发生转换,数据类型一般朝着浮点精度高、长度更长的方向转换,整数型如果转换为signed不会丢失信息,就转换为signed,否则转换为unsigned。
K&R C所采用无房户后保留原著,就是当一个无符号类型与int或更小的整型混合使用时,结果类型是无符号类型。
2 C语言中const并不真正表示常量。
3 switch语句的缺点
1)switch语句最大的缺点是它不会在每个case标签后面的语句执行完毕后自动终止。
2)由于break语句事实上跳出的是最近的那层循环语句或switch语句,所以break可能使switch语句提前跳出结束。
4 C语言中的符号重载
符号 | 意义 |
static |
在函数内部,表示该变量的值在各个调用间一直保持延续性 在函数这一级,表示该函数只对本文件可见 |
extern |
用于函数定义,表示全局可见(属于冗余的) 用于变量,表示它在其他地方定义 |
void |
作为函数的返回类型,表示不返回任何值 在指针声明中,表示通用指针的类型 位于参数列表中,表示没有参数 |
* |
乘法运算符 用于指针,间接引用 在声明中,表示指针 |
& |
位的AND操作符 取地址运算符 |
= | 赋值符 |
== | 比较运算符 |
<= <<= |
小于运算符 左移复合赋值运算符 |
< |
小于运算符 #include指令的左定界符 |
() |
在函数定义中,包围形式参数表 调用一个函数 改变表达式的运算次序 将值转换为其他类型(强制类型转换) 定义带参数的宏 包围sizeof操作符的操作数(如果它是类型名) |
5 C语言中的优先级
优先级问题 | 表达式 | 人们可能误以为的结果 | 实际结果 |
.的优先级高于*。->操作符 用于消除这个问题 |
*p.f | p所指对象的字段f (*p).f |
对p取f偏移,作为左值,然后进行 解除引用操作。*(p.f) |
[]高于* | int *ap[] | ap是个指向int数组的指针 int(*ap)[] | ap是个元素为int左值的数组int *(ap[]) |
函数()高于* | int *fp() | fp是个函数指针,所指函数返回int,int(*fp)() | fp是个函数,返回int*,int*(fp()) |
==和!=高于位运算符 | (val&mask!=0) | (val&mask)!=0 | val&(mask!=0) |
==和!=高于赋值符 | c=getchar()!=EOF | (c=getchar())!=EOF | c=(getchar()!=EOF) |
算术运算符高于移位运算符 | msb<<4+lsb | (msb<<4)+lsb | msb<<(4+lsb) |
逗号运算符在所有运算符中优先级最低 | i=1,2 | i=(1,2) | (i=1),2 |
结合性只用于表达式中出现两个以上相同优先级的操作符的情况,用于消除歧义。事实上,你会注意所有优先级相同的操作符,它们的结合性也相同。
6 返回局部对象的指针
char * localized_time(char* filename)
{
struct tm *tm_ptr;
struct stat stat_block;
char buffer[120];
stat(filename,&stat_block);
tm_ptr=localtime(&stat_block.st_mtime);
strftime(buffer,sizeof(buffer,"%a %b %e %T %Y",tm_ptr);
return buffer;
}
问题就出在最后一行,也就是返回buffer的那行。buffer是一个自动分配内存的数组,是该函数的局部变量。当控制流离开声明自动变量(即局部变量)的范围时,自动变量就会失效。这就意味着即使返回一个指向局部变量的指针,当函数结束时,由于该变量已被销毁,谁也不知道这个指针所指向的地址的内容是什么。
在C语言中,自动变量在堆栈中分配内存。当包含自动变量的函数或代码块退出时,它们所占用的内存便被回收,它们的内容肯定会被下一个调用的函数覆盖。这一切取决于堆栈中先前的自动变量位于何处,活动函数声明了什么变量,写入了什么内容等。原先自动变量地址的内容可能被立即覆盖,也可能稍后才被覆盖。
解决这种问题有几种方案:
1 返回一个指向字符串常量的指针。例如:
char* func() { return "Only works for simple strings";}
2 使用全局声明的数组。例如:
char *fun() {
...
my_global_array[i] =
...
return my_global_array;
}
这个适用于自己创建字符串的情况,也很简单易用。它的缺点在于任何人都有可能在任何时候修改这个全局数组,而且该函数的下一次调用也会覆盖该数组的内容。
3 使用静态数组。例如:
char * func()
{
static char buffer[20];
...
return buffer;
}
这就可以防止任何人修改这个数组。只有拥有指向该数组的指针的函数才能修改这个静态数组。但是,该函数的下一次调用将覆盖这个数组的内容,所以调用者必须在此之前使用或备份数组的内容。和全局数组一样,大型缓冲区如果闲置不用是非常浪费内存空间的。
4 显式分配一些内存,保存返回的值。例如:
char* func(){
char *s=malloc(120);
...
return s;
}
这个方法具有静态数组的优点,而且在每次调用时都创建一个新的缓冲区,所以该函数以后的调用不会覆盖以前的返回值。它适用于多线程的代码。它的缺点在于程序员必须承担内存管理的责任。根据程序的复杂程度,这项任务可能很容易,也可能很复杂。如果内存尚在使用就释放或者出现“内存泄露”(不再使用的内存未回收),就会产生令人难以置信的Bug。
5最好使用的解决方案就是要求调用者分配内存来保存函数的返回值。为了提高安全调用者应该同时指定缓冲区的大小。
void func(char* result,int size){
...
strncpy(result,"That't be in the data segment,Bob",size);
}
buffer=malloc(size);
func(buffer,size);
...
free(buffer);
如果程序员可以在同一代码块中同时进行“malloc”和“free”操作,内存管理是最为轻松的。这个解决方案就可以实现这一点。
7 声明是如何形成的
不合法的声明:
- 函数的返回值不能是一个函数,所以像foo()()这样是非法的。
- 函数的返回值不能是一个数组,所以像foo()[]这样是非法的。
- 数组里面不能有函数,所以像foo[]()这样是非法的。
合法的声明:
- 函数的返回值允许是一个函数指针,如: int(*fun())();
- 函数的返回值允许是一个指向数组的指针,如: int(*foo())[];
- 数组里面允许有函数指针,如int(*foo[])()
- 数组里面允许有其他数组,所以你经常看到int foo[][]
8 typedef的使用
typedef与define的区别:
首先,可以用其他类型说明符对宏类型名进行扩展,但对typedef所定义的类型名不能这样做。如下所示:
#define peach int
unsigned peach i; //没问题
typedef int banana;
unsigned banana i; //错误! 非法
其次,在连续几个变量的声明中,用typedef定义的类型能够保证声明中所有的变量均为同一种类型,而用#define定义的类型则无法保证。如下所示:
#define int_ptr int *;
int_ptr chalk,cheese;
经过宏扩展,第二行变为:
int * chalk,cheese;
这使得chalk和cheese成为不同的类型:chalk是一个指向int的指针,而cheese则是一个int。相反,下面的代码中:
typedef char* char_ptr;
char_ptr Bentley,Rolls_Royce;
Bentley和Rolls_Royce的类型依然相同。虽然前面的类型名变了,但它们的类型相同,都是指向char的指针。
//下面两个声明具有相似的形式
typedef struct fruit{ int weight, price_per_lb; } fruit; //语句1
struct veg{ int weight,price_per_lb; } veg; //语句2
但它们代表的意思却完全不一样,语句1声明了结构表情“fruit”和由“typedef声明的结构类型”fruit“,其实际效果如下:
struct fruit mandarin; //使用结构标签fruit
fruit mandarin; //使用结构类型fruit
语句2声明了结构标签veg和变量veg,只有结构标签能够在以后的声明中使用,如
struct veg potato;
如果试图使用veg cabbage这样的声明,将是一个错误。这有点类似下面的写法:
int i;
i j;
9 声明和定义的区别
记住,C语言的对象必须有且只有一个定义,但它可以有多个extern声明。
定义是一种特殊的声明,它创建了一个对象;声明简单地说明了在其他地方创建的对象的名字,它允许你使用这个名字。
定义 只能出现在一个地方 确定对象的类型并分配内存,用于创建的对象。例如,int my_array[100];
声明 可以多次出现 描述对象的类型,用于指代其他地方定义的对象(例如在其他文件里)例:extern int my_array[];
区分定义和声明
只要记住下面的内容即可分清定义和声明:
声明相当于普通的声明:它所声明的并非自身,而是描述其他地方的创建的对象。
定义相当于特殊的声明:它为对象分配内存。
extern对象声明告诉编译器对象的类型和名字,对象的内存分配则在别处进行。由于并未在声明中为数组分配内存,所以并不需要提供关于数组长度的信息。对于多维数组,需要提供除最左边一维之外其他维的长度——这就给编译器足够的信息产生相应的代码。
10 数组和指针的访问
C语言引入了”可修改的左值“这个术语,它表示左值允许出现在赋值语句的左边,这个奇怪的术语是为与数组名区分,数组名也用于确定对象在内存中的位置,也是左值,但它不能作为赋值的对象。因此,数组名是个左值但不是可修改的左值。
左值:出现在赋值符左边的符号有时被称为左值。
右值:出现在赋值符右边的符号有时则被称为右值。
编译器为每个变量分配一个地址(左值)。这个地址在编译时可知,而且该变量在运行时一直保存于这个地址。相反,存储于变量中的值(它的右值)只有在运行时才可知。如果需要用到变量中存储的值,编译器就发出指令从指定的地址读入变量值并将它存于寄存器中。
这里的关键之处在于每个符号的地址在编译时可知。所以,如果编译器需要一个地址(可能还需要加上偏移量)来执行某种操作,它就可以直接进行操作,并不需要增加指令首先取得具体的地址。相反,对于指针,必须首先在运行时取得它的当前值,然后才能对它进行解除引用的操作(作为以后进行查找的步骤之一)。
相反,如果声明extern char *p,它将告诉编译器p是一个指针,它指向的对象是一个字符。为了取得这个字符,必须得到地址p的内容,把它作为字符的地址并从这个地址中取得这个字符。指针的访问要灵活很多,但需要增加一个额外的提取,如图所示:
11 数组和指针的其他区别
比较数组和指针的另外一个方法就是对比两者的特点。
数组和指针的区别
指针 | 数组 |
保存数据的地址 |
保存数据 |
间接访问数据,首先取得指针的内容,把它作为地址,然后从这个地址提取数据。 如果指针有一个下标[I],就把指针的内容加上I作为地址,从中提取数据 |
直接访问,a[I]只是简单地从a+I为地址取得数据 |
通常用于动态数据结构 | 通常用于存储固定数目且数据类型相同的元素 |
相关的函数为malloc() free() | 隐式分配和删除 |
通常指向匿名数据 | 自身即为数据名 |
定义指针时,编译器并不为指针所指向的对象分配空间,它只是分配指针本身的空间,除非在定义时赋给指针一个字符串常量进行初始化。例如,下面的定义创建了一个字符串常量(为其分配了内存):
char *p="breadfruit";
注意只有对字符串常量才是如此。不能指望为浮点数之类的常量分配空间,如:
float *pip=3.141; //错误
初始化指针时所创建的字符串常量被定义为只读的。如果试图通过指针修改这个字符串的值,程序就会出现未定义的行为。
数组也可以用字符串常量进行初始化:
char a[]="gooseberry";
与指针相反,由字符串常量初始化的数组是可以修改的。
12 UNIX中的堆栈段和MS-DOS中的堆栈段
在UNIX中,当进程需要更多空间时,堆栈会自动生长。程序员可以想象堆栈是无限大的。这是UNIX胜过其他操作系统如MS-DOS的许多优势之一。在UNIX的实现中一般使用某种形式的虚拟内存。当试图访问当前系统分配给堆栈的空间之外时,它将产生一个硬件中断,称为页错误。处理页错误的方法有好几种,取决于对页的引用是否有效。
在正常情况下,内核通过向违规的进程发送合适的信号(可能是段错误)来处理对地址的引用。在堆栈顶部的下端有一个称为red zone的小型区域,如果对这个区域进行引用,不会产生失败。相反,操作系统通过一个好的内存块来增加堆栈段的大小。
在DOS中,在建立可执行文件时,堆栈的大小必须同时确定,而且它不能在运行时增加,如果你猜测错误,需要的堆栈空间大于所分配的空间,那么你和程序都会迷失。如果设置了检查选项,就会收到STACK OVERFLOW(堆栈溢出)消息。
13 返回局部指针的两种不同情况。
先看下面的程序:
#include<iostream>
using namespace std;
char* test2()
{
char p[] = "hello world";
return p;
}
char* test3(){
char *p = "hello world";
return p;
}
int main()
{
test2();
test3();
}
其中,test2中p是一个数组,分配在栈空间,在栈空间存放字符串。当是数组p,则函数会将字符串常量的字符逐个复制到p数组里面,返回p则是返回数组p,但是调用函数结束后p被销毁,里面的元素不存在了。
但是在test3中p是一个指针,则p指向存放字符串常量的地址,返回p则是返回字符串常量地址值,调用函数结束字符串常量不会消失(是常量)。所以返回常量的地址不会出错。
虽然都是返回局部变量的指针,但是test2结果会有问题,但是test3不会。
面试总结-进程、线程与多线程
线程、进程和多线程是面试过程中很容易遇到的问题,去年百度三面的时候就遇到这个问题,当时百度hr问到:线程和进程的区别是什么?你写过多进程或者多线程的程序吗?在你看来多线程和多进程程序那种程序比较难写?
第一个问题很常规,但是要完全答对却不是那么容易,现在想来,第二个问题和第三个问题实际是一个问题,因为第三个问题是验证第二个问题是否说实话的最好的方式。另外,在今年4月6号微软的笔试中,也考到了这第一个个问题,那么,这么方面到底会有哪些问题呢?如果要复习从哪些方面着手呢?
首先,从面试问题的角度,经过网上搜集,我认为会被问到如下问题。
1.进程和线程有什么区别?
这个一个最常见,却最不好回答的问题,csdn上面一位博主给出的解答和另一位cnblog博主的解答稍微清晰些一些,总结起来,就是一下的几个区别:
a.进程是资源分配的基本单位,线程是cpu调度,或者说是程序执行的最小单位。在Mac、Windows NT等采用微内核结构的操作系统中,进程的功能发生 了变化:它只是资源分配的单位,而不再是调度运行的单位。在微内核系统中,真正调度运行的基本单位是线程。因此,实现并发功能的单位是线程。
b.进程有独立的地址空间,比如在linux下面启动一个新的进程,系统必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据 段,这是一种非常昂贵的多任务工作方式。而运行一个进程中的线程,它们之间共享大部分数据,使用相同的地址空间,因此启动一个线程,切换一个线 程远比进程操作要快,花费也要小得多。当然,线程是拥有自己的局部变量和堆栈(注意不是堆)的,比如在windows中用_beginthreadex创建一个新 进程就会在调用CreateThread的同时申请一个专属于线程的数据块(_tiddata)。
c.线程之间的通信比较方便。统一进程下的线程共享数据(比如全局变量,静态变量),通过这些数据来通信不仅快捷而且方便,当然如何处理好这些访问 的同步与互斥正是编写多线程程序的难点。而进程之间的通信只能通过进程通信的方式进行。
d.由b,可以轻易地得到结论:多进程比多线程程序要健壮。一个线程死掉整个进程就死掉了,但是在保护模式下,一个进程死掉对另一个进程没有直接影 响。
e.线程的执行与进程是有区别的。每个独立的线程有有自己的一个程序入口,顺序执行序列和程序的出口,但是线程不能独立执行,必须依附与程序之中, 由应用程序提供多个线程的并发控制。
2. 什么是线程安全?(2012年5月百度实习生面试)
如果多线程的程序运行结果是可预期的,而且与单线程的程序运行结果一样,那么说明是“线程安全”的。
3. 秒杀多线程中的题目 解答
另外,这个网址里面讲操作系统的知识倒是挺详实的,还有另外一种解释线程概念
b.多线程的几种实现方法分别是什么?
这个貌似在java面试中会出现,我是专注于c++的,无视掉,但是不得不说,秒杀多线程面试题系列真心是个好总结
c.多线程同步与互斥有几种实现方法?都是什么?(C++)
临界区(CS:critical section)、事件(Event)、互斥量(Mutex)、信号量(semaphores),需要注意的是,临界区是效率最高的,因为基本不需要其 他的开销,二内核对象涉及到用户态和内核态的切换,开销较大,另外,关键段、互斥量具有线程所有权的概念,因此只可以用于线程之间互斥,而不能用到 同步中。只有互斥量能完美解决进程意外终止所造成的“遗弃问题”。
d.多线程同步和互斥有何异同,在什么情况下分别使用他们?举例说明
所谓同步,表示有先有后,比较正式的解释是“线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个 线程的消息时应等待,直到消息到达时才被唤醒。”所谓互斥,比较正式的说明是“线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当 有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥 可以看成是一种特殊的线程同步。”表示不能同时访问,也是个顺序问题,所以互斥是一种特殊的同步操作。
举个例子,设有一个全局变量global,为了保证线程安全,我们规定只有当主线程修改了global之后下一个子线程才能访问global,这就需要同步主线程与子 线程,可用关键段实现。当一个子线程访问global的时候另一个线程不能访问global,那么就需要互斥。
e.以下多线程对int型变量x的操作,哪几个需要进行同步:
A. x=y; B. x++; C. ++x; D. x=1;
答案是ABC,显然,y的写入与x读y要同步,x++和++x都要知道x之前的值,所以也要同步。
f.多线程中栈与堆是公有的还是私有的
A:栈公有, 堆私有
B:栈公有,堆公有
C:栈私有, 堆公有
D:栈私有,堆私有
答案是C,栈一般存放局部变量,而程序员一般自己申请和释放堆中的数据(详见堆与栈的区别)。
g.在Windows编程中互斥量与临界区比较类似,请分析一下二者的主要区别。
针对这个题目的话,答案主要有以下几点:
1)互斥量是内核对象,所以它比临界区更加耗费资源,但是它可以命名,因此可以被其它进程访问
2)从目的是来说,临界区是通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
互斥量是为协调共同对一个共享资源的单独访问而设计的。
h.一个全局变量tally,两个线程并发执行(代码段都是ThreadProc),问两个线程都结束后,tally取值范围。
inttally = 0; //global voidThreadProc() { for (inti = 1; i <= 50; i++) tally += 1; } |
当两线程串行时,结果最大为100,当某个线程运行结束,而此时另外一个线程刚取出0,还未计算时,结果最小为50。