[找工作]程序员面试宝典【笔记】(part 1)

时间:2022-05-31 15:04:18

v  题型:

数据结构(链表,数,图)、C++编程基础、数组、递归、矩阵、内存管理、时间复杂度、TCP/IP、操作系统、计算机网络、UML、OOA&OOP、自己的项目

v  swap的几种写法:

void swap(int* a, int* b)

{  int temp;

  temp = *a;

  *a = *b;

  *b = temp; }

{  *a = *a + *b;

   *b = *a - *b;

   *a = *a - *b; }

{  *a = *a ^ *b;

   *b = *b ^ *a;

   *a = *a ^ *b; }

v  如何在C++中调用C程序:

C++和C是两种完全不同的编译链接处理方式,如果直接在C++里面调用C函数,会找不到函数体,报链接错误。要解决这个问题,就要在 C++文件里面显示声明一下哪些函数是C写的,要用C的方式来处理。
1.引用头文件前需要加上 extern “C”,如果引用多个,那么就如下所示
extern “C”
{
#include “ s.h”
#include “t.h”
#include “g.h”
#include “j.h”
};
然后在调用这些函数之前,需要将函数也全部声明一遍。
2.C++调用C函数的方法,将用到的函数全部重新声明一遍
extern “C”
{
extern void A_app(int);
extern void B_app(int);
extern void C_app(int);
extern void D_app(int);

}

C++程序中调用被c编译器编译后的函数,为什么要加extern "C"?

C++语言支持函数重载,C语言不支持函数重载。函数被C++编译后在库中的名字与C语言的不同。假设某个C 函数的声明如下:
void foo(int x, int y);
该函数被C 编译器编译后在库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字用来支持函数重载和类型安全连接。由于编译后的名字不同,C++程序不能直接调用C 函数。C++提供了一个C 连接交换指定符号extern“C”来解决这个问题。例如:
extern “C”
{
void foo(int x, int y);
// 其它函数
}
或者写成
extern “C”
{
#include “myheader.h”
// 其它C 头文件
}
这就告诉C++编译译器,函数foo 是个C 连接,应该到库中找名字_foo而不是找_foo_int_int。C++编译器开发商已经对C 标准库的头文件作了extern“C”处理,所以我们可以用#include直接引用这些头文件。

v  操作符优先级

优先级

运算符

名称或含义

使用形式

结合方向

说明

1

[]

数组下标

数组名[常量表达式]

左到右

 

()

圆括号

(表达式)/函数名(形参表)

 

.

成员选择(对象)

对象.成员名

 

->

成员选择(指针)

对象指针->成员名

 

2

-

负号运算符

-表达式

右到左

单目

(类型)

强制类型转换

(数据类型)表达式

 

++

自增运算符

++变量名/变量名++

单目

--

自减运算符

--变量名/变量名--

单目

*

取值运算符

*指针变量

单目

&

取地址运算符

&变量名

单目

!

逻辑非运算符

!表达式

单目

~

按位取反运算符

~表达式

单目

sizeof

长度运算符

sizeof(表达式)

 

3

/

表达式/表达式

左到右

双目

*

表达式*表达式

双目

%

余数(取模)

整型表达式/整型表达式

双目

4

+

表达式+表达式

左到右

双目

-

表达式-表达式

双目

5

<< 

左移

变量<<表达式

左到右

双目

>> 

右移

变量>>表达式

双目

6

大于

表达式>表达式

左到右

双目

>=

大于等于

表达式>=表达式

双目

小于

表达式<表达式

双目

<=

小于等于

表达式<=表达式

双目

7

==

等于

表达式==表达式

左到右

双目

!=

不等于

表达式!= 表达式

双目

8

&

按位与

表达式&表达式

左到右

双目

9

^

按位异或

表达式^表达式

左到右

双目

10

|

按位或

表达式|表达式

左到右

双目

11

&&

逻辑与

表达式&&表达式

左到右

双目

12

||

逻辑或

表达式||表达式

左到右

双目

13

?:

条件运算符

表达式1? 表达式2: 表达式3

右到左

三目

14

=

赋值运算符

变量=表达式

右到左

 

/=

除后赋值

变量/=表达式

 

*=

乘后赋值

变量*=表达式

 

%=

取模后赋值

变量%=表达式

 

+=

加后赋值

变量+=表达式

 

-=

减后赋值

变量-=表达式

 

<<=

左移后赋值

变量<<=表达式

 

>>=

右移后赋值

变量>>=表达式

 

&=

按位与后赋值

变量&=表达式

 

^=

按位异或后赋值

变量^=表达式

 

|=

按位或后赋值

变量|=表达式

 

15

,

逗号运算符

表达式,表达式,…

左到右

从左向右顺序运算

v  ::在C++中是什么意思 

::是运算符中等级最高的,它分为三种:
1)global scope(全局作用域符),用法(::name)
2)class scope(类作用域符),用法(class::name)
3)namespace scope(命名空间作用域符),用法(namespace::name)
他们都是左关联(left-associativity)
他们的作用都是为了更明确的调用你想要的变量,如在程序中的某一处你想调用全局变量a,那么就写成::a,如果想调用class A中的成员变量a,那么就写成A::a,另外一个如果想调用namespace std中的cout成员,你就写成std::cout(相当于using namespace std;cout)意思是在这里我想用cout对象是命名空间std中的cout(即就是标准库里边的cout)

v  i++

!x++;     !与++优先级相同,先计算!x;然后计算x++; (原题:P35)

v  指针+1的问题

指针变量加1,即向后移动1 个位置表示指针变量指向下一个数据元素的首地址。而不是在原地址基础上加1。至于真实的地址加了多少,要看原来指针指向的数据类型是什么。

p指向的是一个字符,p+1就是移动一个字符大小,一个字符就是一个字节,所以p +1 代表的地址就比 p 代表的地址大1。

p指向的是一个整型,p+1就是移动一个整型大小,即移动4个字节,所以p+1代表的地址比p代表的地址大4.

v  x++与++x

#include <stdio.h>

void main()

{

    int arr[] = { 6, 7, 8, 9, 10 };

    int *ptr = arr;

    *(ptr++) += 123;//equal:*ptr=*ptr+123;ptr++;

    printf("%d,%d\n",*ptr,*(++ptr));//prinft计算参数时从右向左入栈,先执行++ptr;

}

RESULT:8,8

X++先执行运算最后++;++X则先++然后执行其他运算;

v  隐式类型转换发生在下列这些典型的情况下:

  1. 在混合类型的算术表达式中(最宽的数据类型成为目标类型)。
  2. 用一种类型的表达式赋值给另一种类型的对象。
  3. 把一个表达式传递给一个函数,调用表达式的类型与形式参数的类型不同。
  4. 从一个函数返回一个表达式的类型与返回类型不相同。

v  判断一个数X是否是2N次方,不可用循环语句:

!(X&(X-1))==0

v  Const

在C程序中,const的用法主要是定义常量、修饰函数参数、修饰函数返回值,在C++中,它还可以修饰函数的定义体,定义类中某个成员函数为恒态函数,即不改变类中的数据成员

被Const修饰的东西可以受到强制保护,预防意外的变动,能提高程序的健壮性

Const与#define相比有什么不同?

Const常量有数据类型,而宏常量没有数据类型,编译器可以对前者进行类型安全检查,而对后者只能进行字符替换,没有类型安全检查,并且在字符替换中可能产生意料不到的错误

调试工具能对const常量进行调试,但是无法对define常量调试

No.

作用

说明

参考代码

1

可以定义const常量

 

const int Max = 100; 

2

便于进行类型检查

const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换时可能会产生意料不到的错误

void f(const int i) { .........}
      //对传入的参数进行类型检查,不匹配进行提示

3

可以保护被修饰的东西

防止意外的修改,增强程序的健壮性。

void f(const int i) { i=10;//error! }
      //如果在函数体内修改了i,编译器就会报错

4

可以很方便地进行参数的调整和修改

同宏定义一样,可以做到不变则已,一变都变

 

5

为函数重载提供了一个参考

 

class A
{
           ......
  void f(int i)       {......} //一个函数
  void f(int i) const {......} //上一个函数的重载
           ......
};

6

可以节省空间,避免不必要的内存分配

const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝

#define PI 3.14159         //常量宏
const doulbe  Pi=3.14159;  //此时并未将Pi放入ROM中
              ......
double i=Pi;   //此时为Pi分配内存,以后不再分配!
double I=PI;  //编译期间进行宏替换,分配内存
double j=Pi;  //没有内存分配
double J=PI;  //再进行宏替换,又一次分配内存!

7

 提高了效率

编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高

 

const int *A; 或 int const *A;  //const修饰指向的对象,A可变,A指向的对象不可变
int *const A;              //const修饰指针A, A不可变,A指向的对象可变
const int *const A;          //指针A和A指向的对象都不可变

v sizeof与Strlen()

sizeof   单位:字节

sizeof(...)是运算符,而不是一个函数。一个简单的例子:
int a;
cout<<sizeof a<<endl;

在头文件中typedef为unsigned int,其值在编译时即计算好了,参数可以是数组、指针、类型、对象、函数等。

它的功能是:获得保证能容纳实现所建立的最大对象的字节大小

由于在编译时计算,因此sizeof不能用来返回动态分配的内存空间的大小。
实际上,用sizeof来返回类型以及静态分配的对象、结构或数组所占的空间,返回值跟对象、结构、数组所存储的内容没有关系。
具体而言,当参数分别如下时,sizeof返回的值表示的含义如下:数组——编译时分配的数组空间大小;
指针——存储该指针所用的空间大小(存储该指针的地址的长度,是长整型,应该为4);类型——该类型所占的空间大小;

对象——对象的实际占用空间大小;
函数——函数的返回类型所占的空间大小。函数的返回类型不能是void。
********************************************************************

strlen
strlen(...)是函数,要在运行时才能计算。

参数必须是字符型指针(char*, 且必须是以'\0'结尾的。当数组名作为参数传入时,实际上数组就退化成指针了。

int ac[10];

cout<<sizeof(ac)<<endl;
cout<<strlen(ac)<<endl;     (ac相当于一个指针,但是strlen只能接受char*类型,所以编译时出错)

它的功能是:返回字符串的长度。该字符串可能是自己定义的,也可能是内存中随机的,该函数实际完成的功能是从代表该字符串的第一个地址开始遍历,直到遇到结束符'\0'。返回的长度大小不包括'\0'。

 

v 结构体长度对齐

在默认情况下,为了方便对结构体内元素的访问和管理,当结构体内的元素长度都小于处理器的位数的时候,便以结构体里面最长的数据元素为对其单位,也就是说,结构体的长度一定是最长的数据元素的整数倍;如果结构体内存在长度大于处理器位数的元素,那么就以处理器的位数为对齐单位。但是结构体内类型相同的连续元素将在连续的空间内,和数组一样。

CPU的优化规则大致是这样:对于n字节的元素(n=2,4,8,…),他的首地址能被n整除,才能获得最好的性能。(个人感觉是如果不是这样,那么一个元素很有可能会跨页存储,存在两个不同的页面上,导致访问速度慢)

v 对其是一种以空间换时间的方法,在访问内存时,如果地址按4字节对齐,则访问效率会高很多,这种现象的原因在于访问内存的硬件电路。一般情况下,地址总线总是按照对齐后的地址来访问的。例如你想得到0x00000001开始的4字节内容,系统需先以0x00000000读4个字节,从中取3字节,然后再用0X00000004最为开始地址,获得下一个四字节,再从中得到第一个字节,两次组合出你想要的内容。但是如果地址一开始就是对齐到0x00000000,则系统只需一次内存读写即可。

v 对其的自定义设置

#pragma pack(1)

struct aStruct…{…};

#pragma pack()

v 字节对齐是在编译时决定的,不会再发生变化,在运行时不会再发生变化

v 静态变量存放在全局数据区,而sizeof计算栈中分配的大小,是不会计算static变量的。

v 数据类型的长度:

short -------------- 2

int,long,float,指针---4

double -------------- 8

v sizeof vs strlen

sizeof是算符,strlen是函数

sizeof可以用类型做参数,strlen只能用char*做参数,且必须是以“\0”结尾的。

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

sizeof在编译时计算,strlen在运算时计算

对函数使用sizeof,在编译阶段会被函数返回值类型取代。

v sizeof不是函数,也不是一元运算符,他是个类似宏定义的特殊关键字,sizeof()括号内的内容是不被编译的,只是替换,所以a=8;sizeof(a=6);之后a的值仍然为8。

v unsigned影响的仅仅是最高位bit的意义(正/负),而不会影响数据长度。

v 空类所占空间为1,单一继承的空类空间也为1,多重继承的空类空间还是1,但是虚继承涉及到虚表(虚指针),所以空间大小为4

 

v 内联函数和宏定义

内联函数和普通函数相比可以加快程序运行的速度,因为不需要中断调用,在编译的时候内联函数可以直接被镶嵌到目标代码中,而宏只是一个简单的替换;内联函数要做参数类型检查,这是inline和宏相比的优势;

inline一般只用于如下情况:

  1. 一个函数不断被重复调用;
  2. 函数只有简单的几行,且函数内不包括for/while/switch语句;

—————————————————————————————————————————————————————————————————————————————————————————

v 指针问题:包括常量指针、数组指针、函数指针、this指针、指针传值、指向指针的指针等问题

v 指针和引用的区别

非空区别:在任何情况下都不能使用指向空值的引用;

合法性区别:在使用引用之前不需要测试它的合法性;

可修改区别:指针可以被重新赋值以指向另一个不同的对象,但是引用则总是指向初始化时被指定的对象,以后不能改变,但是指定的内容可以改变;

应用区别:使用指针:存在不指向任何对象的可能;需要 在不同的时刻指向不同的对象;使用引用:总是指向一个对象并且一旦指向一个对象后就不会改变指向。

v 引用的初始化:

声明一个引用时,必须同时初始化,如同const常量必须声明的同时初始化。

v char c[] vs char *c

char c[]分配的是局部数组,而char *c分配的是全局数组

 

v 函数指针

函数指针

void (*f)()

函数返回指针

void* f()

const指针

cons int *

指向const的指针

int * const

指向const的const指针

cons int* const

v malloc和free是C/C++的标准库函数,new/delete是C/C++的运算符。他们都可以用于动态申请内存和释放内存。对于非内部数据类型的对象而言,光用malloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限范围内,不能把执行构造函数和析构函数的任务强加于malloc/free

v 数组指针和指针数组:

数组指针

指针数组

int (*a)[]

int *a[]

 

—————————————————————————————————————————————————————————————————————————————————————————

v STL模版与容器

_________________

v STL的优点:

  • 方便容易的实现搜索数据或对数据排序等一系列的算法
  • 调试程序是更安全和方便
  • 即使是人们用STL在UNIX平台下写的代码,你也可以很容易理解

v STL中的一些基本概念

  • 模版(Template)类的宏(macro),正规名:泛型,类的模版-泛型类,函数模版-泛型函数
  • STL标准模版库,一些聪明人自己写的一些模版
  • 容器(Container):可容纳一些数据的模版类,STL中有vector, set, map, multimap 和 deque等容器
  • 向量(Vector):基本数组模版,是一个容器
  • 游标(Iterator):它是一个指针,用来指向STL容器中的元素,也可以指向其他元素

v 容器向量:标准模板库是一个基于模版的容器类库,包括链表、列表、队列和堆栈;标准模板库还包括许多常用的算法,包括排序和查找;可重用;容器是包括其他对象的对象;标准模版库类有两种类型:顺序和关联;不同OS间可移植;

v vector的操作:

标准库vector类型使用需要的头文件:#include <vector>。vector 是一个类模板。不是一种数据类型,vector<int>是一种数据类型。Vector的存储空间是连续的,list不是连续存储的。

  • 定义和初始化
    vector< typeName > v1;      //默认v1为空,故下面的赋值是错误的v1[0]=5;
    vector<typeName>v2(v1);  或v2=v1 ; 或vector<typeName>v2(v1.begin(), v1.end());//v2是v1的一个副本,若v1.size()>v2.size()则赋值后v2.size()被扩充为 v1.size()。
    vector< typeName > v3(n,i);//v3包含n个值为i的typeName类型元素
    vector< typeName > v4(n); //v4含有n个值为0的元素
    int a[4]={0,1,2,3,3}; vector<int> v5(a,a+5);//v5的size为5,v5被初始化为a的5个值。后一个指针要指向将被拷贝的末元素的下一位置。
    vector<int> v6(v5);//v6是v5的拷贝。
    vector< 类型 > 标识符(最大容量,初始所有值)。

 

  • 值初始化
    1>     如果没有指定元素初始化式,标准库自行提供一个初始化值进行值初始化。
    2>     如果保存的是含有构造函数的类类型的元素,标准库使用该类型的构造函数初始化。
    3>     如果保存的是没有构造函数的类类型的元素,标准库产生一个带初始值的对象,使用这个对象进行值初始化。
  • vector对象最重要的几种操作
    1. v.push_back(t)   在容器的最后添加一个值为t的数据,容器的size变大。另外list有push_front()函数,在前端插入,后面的元素下标依次增大。
    2. v.size()    返回容器中数据的个数,size返回相应vector类定义的size_type的值。v.resize(2*v.size)或v.resize(2*v.size, 99) 将v的容量翻倍(并把新元素的值初始化为99)
    3. v.empty()     判断vector是否为空
    4. v[n]          返回v中位置为n的元素
    5. v.insert(pointer,number, content)   向v中pointer指向的位置插入number个content的内容。
     还有v. insert(pointer, content),v.insert(pointer,a[2],a[4])将a[2]到a[4]三个元素插入。
    6. v.pop_back()   删除容器的末元素,并不返回该元素。
    7.v.erase(pointer1,pointer2) 删除pointer1到pointer2中间(包括pointer1所指)的元素。vector中删除一个元素后,此位置以后的元素都需要往前移动一个位置,虽然当前迭代器位置没有自动加1,但是由于后续元素的顺次前移,也就相当于迭代器的自动指向下一个位置一样。
    8. v1==v2         判断v1与v2是否相等。
    9. !=、<、<=、>、>=     保持这些操作符惯有含义。
    10. vector<typeName>::iterator p=v1.begin( ); p初始值指向v1的第一个元素。*p取所指向元素的值。对于const vector<typeName>只能用vector<typeName>::const_iterator类型的指针访问。
    11.   p=v1.end( ); p指向v1的最后一个元素的下一位置。
    12.v.clear()    删除容器中的所有元素。

 

  • #include<algorithm>中的泛函算法
    搜索算法:find() 、search() 、count() 、find_if() 、search_if()、count_if()
    分类排序:sort() 、merge()
    删除算法:unique() 、remove()
    生成和变异:generate() 、fill() 、transformation() 、copy()
    关系算法:equal() 、min() 、max()
    sort(v1.begin(),vi.begin()+v1.size/2); 对v1的前半段元素排序
    list<char>::iterator pMiddle =find(cList.begin(),cList.end(),'A');找到则返回被查内容第一次出现处指针,否则返回end()。
    vector< typeName >::size_type x ; vector< typeName >类型的计数,可用于循环如同for(int i)

初学C++的程序员可能会认为vector的下标操作可以添加元素,其实不然:

vector<int> ivec;  // empty vector

for (vector<int>::size_typeix = 0; ix != 10; ++ix)

    ivec[ix] = ix; // disaster: ivec has no elements

上述程序试图在ivec中插入10个新元素,元素值依次为0到9的整数。但是,这里ivec是空的vector对象,而且下标只能用于获取已存在的元素。

这个循环的正确写法应该是:

for (vector<int>::size_typeix = 0; ix != 10; ++ix)

    ivec.push_back(ix); // ok: adds new element with value ix
警告:必须是已存在的元素才能用下标操作符进行索引。通过下标操作进行赋值时,不会添加任何元素,仅能对确知已存在的元素进行下标操作 。

v  泛型编程

何谓泛型编程:STL代表用一致的方式编程是可能的;泛型编程是一种基于发现高效算法的最抽象表示的编程方法;STL是一个泛型编程的例子,C++是我们可以实现令人信服的例子的语言。

v  模版

一个函数在编译时被分配给一个入口地址,这个入口地址就称为函数的指针,正如指针是一个变量的地址一样。(经测试:函数名和对函数名取地址得到的是同一个值,都可以作为该函数的指针)。有些地方必须使用函数指针才能完成给定的任务,特别是异步操作的回调和其他需要匿名回调的结构。另外,想线程的执行和时间的处理,如果缺少了函数的支持也是很难完成的。

—————————————————————————————————————————————————————————————————————————————————————————————

v 面向对象(Object-Oriented)

编程是在计算机中反应世界

面向对象的优点:良好的可复用性、易维护、良好的可扩充性

面向对象技术的基本概念是:对象、类和继承

C++中的空类默认产生的成员函数:

默认构造函数、析构函数、拷贝构造函数、赋值函数

C++中struct也可以有constructor/destructor及成员函数,它和class的区别是:class的默认访问控制是private,struct中默认访问控制是public。

在一个类中,初始化列表的初始化变量顺序是根据成员变量的声明顺序来执行的。

MFC库中,为什么CObject的析构函数是虚函数?

保证在任何情况下,不会出现由于析构函数未被调用而导致内存泄漏

析构函数可以是内联函数,但内联函数不能为virtual

 

多态:允许将子类类型的指针赋值给父类类型的指针,多态在ObjectPascal和C++中都是通过虚函数(VirtualFunction)实现的。

 

  • overload vs override:
    • overload: 范围相同,参数不同,静态,函数调用在编译期间确定,早绑定,与面向对象无关,与多态无关。
    • override:范围不同,参数相同,子类重新定义父类的虚函数,函数动态调用、运行期绑定。

 

 

实现代码重用

封装:隐藏实现细节,使得代码模块化

继承:扩展已存在的代码模块(类)

多态:同一操作作用于不同对象上,产生不同的解释和执行结果—实现接口重用

 

友元函数:定义在类外部的普通函数,但他需要在类体内说明,为了与该类的成员函数区别,说明时需要在前面加上friend来区别;他虽然不是成员函数,但却可以访问类的私有变量。

友元还可以是类,称为友元类

 

继承:可以使一个新类获得其父类的操作和数据结构,程序员只需在新类中增加原有类中没有的成分。

 

为了不破坏类的封装性,所有操作应该通过接口进行沟通。

 

类中的static变量存在数据段,不在类的实例空间中。