1.定义:为变量分配地址和存储空间,声明:不分配地址。
2.一个变量可以在多个地方声明,但是只在一个地方定义。
3.加入 extern 修饰的是变量的声明,说明此变量将在文件以外或在文件后面部分定义。说明:很多时候一个变量,只是声明不分配内存空间,直到具体使用时才初始化,分配内存空间,如外部变量。
//bool 型数据:
if( flag )
{
A;
}
else
{
B;
}
//int 型数据:
if( 0 != flag )
{
A;
}
else {
B;
}
//指针型:
if( NULL == flag )
{
A;
}
else {
B;
}
//float 型数据:
if ( ( flag >= -NORM ) && ( flag <= NORM ) )
{
A;
}
注意:应特别注意在 int、指针型变量和“零值”比较的时候,把“零值”放在左边,这样当把“==” 误写成“=”时,编译器可以报错,否则这种逻辑错误不容易发现,并且可能导致很严重的后果。
sizeof 和 strlen 有以下区别:
1 sizeof 是一个操作符,strlen 是库函数。
2 sizeof 的参数可以是数据的类型,也可以是变量,而 strlen 只能以结尾为‘\0‘的字符串作参数。
3 编译器在编译时就计算出了 sizeof 的结果。而 strlen 函数必须在运行时才能计算出来。并且 sizeof 计算的是数据类型占内存的大小,而 strlen 计算的是字符串实际的长度。
4 数组做 sizeof 的参数不退化,传递给 strlen 就退化为指针了。
注意:有些是操作符看起来像是函数,而有些函数名看起来又像操作符,这类容易混淆的名称一定要加以区分,否则遇到数组名这类特殊数据类型作参数时就很容易出错。最容易混淆为函数的操作符就是 sizeof。
在 C 中 static 用来修饰局部静态变量和外部静态变量、函数。而 C++中除了上述功能外,还用来定义类的成员变量和函数。即静态成员和静态成员函数。
注意:编程时 static 的记忆性,和全局性的特点可以让在不同时期调用的函数进行通信,传递信息,而 C++的静态成员则可以在多个对象实例间进行通信,传递信息。
malloc 和 new 有以下不同:
(1) new、delete 是操作符,可以重载,只能在 C++中使用。
(2) malloc、free 是函数,可以覆盖,C、C++中都可以使用。
(3) new 可以调用对象的构造函数,对应的 delete 调用相应的析构函数。
(4) malloc 仅仅分配内存,free 仅仅回收内存,并不执行构造和析构函数
(5) new、delete 返回的是某种数据类型指针,malloc、free 返回的是 void 指针。
注意:malloc 申请的内存空间要用 free 释放,而 new 申请的内存空间要用 delete 释放,不要混用。
因为两者实现的机理不同。
#define min(a,b)((a)<=(b)?(a):(b))
1
注意:在调用时一定要注意这个宏定义的副作用,如下调用:
((++*p)<=(x)?(++*p):(x)
1
p 指针就自加了两次,违背了 MIN 的本意。
可以,因为指针和普通变量一样,有时也有变化程序的不可控性。常见例:子中断服务子程序修改一个指向一个 buffer 的指针时,必须用 volatile 来修饰这个指针。
说明:指针是一种普通的变量,从访问上没有什么不同于其他变量的特性。其保存的数值是个整型数据,和整型变量不同的是,这个整型数据指向的是一段内存地址。
请写出以下代码的打印结果,主要目的是考察 a 和&a 的区别。
#include
void main( void )
{
int a[5]={1,2,3,4,5};
int ptr=(int )(&a+1);
printf("%d,%d",(a+1),(ptr-1));
return;
}
输出结果:2,5。
这是因为数组在内存中是连续存储的,a+1表示数组的第2个元素的地址。而ptr是一个指针,它指向的是a整个数组在内存中的后一个位置,也就是数组外的地址。所以*(a+1)等于2,表示取数组内的第2个元素。而*(ptr-1)等于5,表示取了指针指向的地址的前一个位置的元素,也就是数组内的最后一个元素。
注意:数组名 a 可以作数组的首地址,而&a 是数组的指针。思考,将原式的 int *ptr=(int *)(&a+1); 改为 int *ptr=(int *)(a+1);时输出结果将是什么呢?
C、C++中内存分配方式可以分为三种:
(1) 从静态存储区域分配:
内存在程序编译时就已经分配好,这块内存在程序的整个运行期间都存在。速度快、不容易出错,因为有系统会善后。例如全局变量,static 变量等。
(2) 在栈上分配:
在执行函数时,函数内局部变量的存储单元都在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
(3) 从堆上分配:
即动态内存分配。程序在运行的时候用 malloc 或 new 申请任意大小的内存,程序员自己负责在何时用 free 或 delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活。如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,另外频繁地分配和释放不同大小的堆空间将会产生堆内碎块。
一个 C、C++程序编译时内存分为 5 大存储区:堆区、栈区、全局区、文字常量区、程序代码区。
三者主要有以下不同之处:
(1) 操作对象不同,strcpy 的两个操作对象均为字符串,sprintf 的操作源对象可以是多种数据类型,目的操作对象是字符串,memcpy 的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。
(2) 执行效率不同,memcpy 最高,strcpy 次之,sprintf 的效率最低。
(3) 实现功能不同,strcpy 主要实现字符串变量间的拷贝,sprintf 主要实现其他数据类型格式到字符串的转化,memcpy 主要是内存块间的拷贝。
说明:strcpy、sprintf 与 memcpy 都可以实现拷贝的功能,但是针对的对象不同,根据实际需求,来选择合适的函数实现拷贝功能。
int *ptr;
ptr = (int *)0x67a9;
*ptr = 0xaa66;
说明:这道题就是强制类型转换的典型例子,无论在什么平台地址长度和整型数据的长度是一样的,即一个整型数据可以强制转换成地址指针类型,只要有意义即可。
面向对象的三大特征是封装性、继承性和多态性:
封装性:将客观事物抽象成类,每个类对自身的数据和方法实行 protection(private, protected, public)。
继承性:广义的继承有三种实现形式:实现继承(使用基类的属性和方法而无需额外编码的能力)、可视继承(子窗体使用父窗体的外观和实现代码)、接口继承(仅使用属性和方法,实现滞后到子类实现)。
多态性:是将父类对象设置成为和一个或更多它的子对象相等的技术。用子类对象给父类对象赋值之后,父类对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。 这部分需要熟悉掌握原理虚函数,了解一些概念(静态多态、动态多态)等,面试时经常会问。
说明:面向对象的三个特征是实现面向对象技术的关键,每一个特征的相关技术都非常的复杂,程序员应该多看、多练。
缺省构造函数。
缺省拷贝构造函数。
缺省析构函数。
缺省赋值运算符。
缺省取址运算符。
缺省取址运算符 const。
注意:有些书上只是简单的介绍了前四个函数。没有提及后面这两个函数。但后面这两个函数也是空类的默认函数。另外需要注意的是,只有当实际使用这些函数的时候,编译器才会去定义它们。
拷贝构造函数和赋值运算符重载有以下两个不同之处:
(1) 拷贝构造函数生成新的类对象,而赋值运算符不能。
(2) 由于拷贝构造函数是直接构造一个新的类对象,所以在初始化这个对象之前不用检验源对象是否和新建对象相同。而赋值运算符则需要这个操作,另外赋值运算中如果原来的对象中有内存分配要先把内存释放掉
注意:当有类中有指针类型的成员变量时,一定要重写拷贝构造函数和赋值运算符,不要使用默认的。
template <typename T> class A
{
friend T; private:
A() {}
~A() {}
};
class B : virtual public A<B>
{
public:
B() {}
~B() {}
};
class C : virtual public B
{
public:
C() {}
~C() {}
};
void main( void )
{
B b; //C c;
return;
}
注意:构造函数是继承实现的关键,每次子类对象构造时,首先调用的是父类的构造函数,然后才是自己的。
写出以下程序的输出结果:
#include
class A
{
virtual void g()
{
cout << "A::g" << endl;
}
private:
virtual void f()
{
cout << "A::f" << endl;
}
};
class B : public A
{
void g()
{
cout << "B::g" << endl;
}
virtual void h()
{
cout << "B::h" << endl;
}
};
typedef void( *Fun )( void ); void main()
{
B b;
Fun pFun;
for(int i = 0 ; i < 3; i++)
{
pFun = ( Fun )*( ( int* ) * ( int* )( &b ) + i );
pFun();
}
}
输出结果:
B::g
A::f
B::h
注意:本题主要考察了面试者对虚函数的理解程度。一个对虚函数不了解的人很难正确的做出本题。
在学习面向对象的多态性时一定要深刻理解虚函数表的工作原理。
(1)重写和重载主要有以下几点不同。
范围的区别:被重写的和重写的函数在两个类中,而重载和被重载的函数在同一个类中。
参数的区别:被重写函数和重写函数的参数列表一定相同,而被重载函数和重载函数的参数列表一定不同。
virtual 的区别:重写的基类中被重写的函数必须要有 virtual 修饰,而重载函数和被重载函数可以被
virtual 修饰,也可以没有。
(2)隐藏和重写、重载有以下几点不同。
与重载的范围不同:和重写一样,隐藏函数和被隐藏函数不在同一个类中。
参数的区别:隐藏函数和被隐藏的函数的参数列表可以相同,也可不同,但是函数名肯定要相同。当参数不相同时,无论基类中的参数是否被 virtual 修饰,基类的函数都是被隐藏,而不是被重写。
说明:虽然重载和覆盖都是实现多态的基础,但是两者实现的技术完全不相同,达到的目的也是完全不同的,覆盖是动态态绑定的多态,而重载是静态绑定的多态。
编译器发现一个类中有虚函数,便会立即为此类生成虚函数表 vtable。虚函数表的各表项为指向对应虚函数的指针。编译器还会在此类中隐含插入一个指针 vptr(对 vc 编译器来说,它插在类的第一个位置上)指向虚函数表。调用此类的构造函数时,在类的构造函数中,编译器会隐含执行 vptr 与 vtable 的关联代码,将 vptr 指向对应的 vtable,将类与此类的 vtable 联系了起来。另外在调用类的构造函数时,指向基础类的指针此时已经变成指向具体的类的 this 指针,这样依靠此 this 指针即可得到正确的 vtable,。
如此才能真正与函数体进行连接,这就是动态联编,实现多态的基本原理。
注意:一定要区分虚函数,纯虚函数、虚拟继承的关系和区别。牢记虚函数实现原理,因为多态
C++面试的重要考点之一,而虚函数是实现多态的基础。
数组和链表有以下几点不同:
(1) 存储形式:数组是一块连续的空间,声明时就要确定长度。链表是一块可不连续的动态空间,长度可变,每个结点要保存相邻结点指针。
(2) 数据查找:数组的线性查找速度快,查找操作直接使用偏移地址。链表需要按顺序检索结点,效率低。
(3) 数据插入或删除:链表可以快速插入和删除结点,而数组则可能需要大量数据移动。
(4) 越界问题:链表不存在越界问题,数组有越界问题。
说明:在选择数组或链表数据结构时,一定要根据实际需要进行选择。数组便于查询,链表便于插入删除。数组节省空间但是长度固定,链表虽然变长但是占了更多的存储空间。
(1) 反转一个链表。循环算法。
List reverse(List n)
{
if(!n) //判断链表是否为空,为空即退出。
{
return n;
}
list cur = n.next; //保存头结点的下个结点
list pre = n;
list tmp; //保存头结点
pre.next = null; //头结点的指针指空,转换后变尾结点
while ( NULL != cur.next ) //循环直到 cur.next 为空
{
tmp = cur;
}
tmp.next = pre;
pre = tmp;
cur = cur.next;
return tmp;//f 返回头指针
}
(2) 反转一个链表。递归算法。
List *reverse( List *oldList, List *newHead = NULL )
{
List *next = oldList-> next; //记录上次翻转后的链表
oldList-> next = newHead; //将当前结点插入到翻转后链表的开头
newHead = oldList; //递归处理剩余的链表
return ( next==NULL )? newHead: reverse( t, newHead );
}
说明:循环算法就是移动过程,比较好理解和想到。递归算法的设计虽有一点难度,但是理解了循环算法,再设计递归算法就简单多了。
队列和栈都是线性存储结构,但是两者的插入和删除数据的操作不同,队列是“先进先出”,栈是
“后进先出”。
注意:区别栈区和堆区。堆区的存取是“顺序随意”,而栈区是“后进先出”。栈由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。堆一般由程序员分配释放, 若程序员不释放,程序结束时可能由 OS 回收。分配方式类似于链表。
它与本题中的堆和栈是两回事。堆栈只是一种数据结构,而堆区和栈区是程序的不同内存存储区域。
//结点结构体:
typedef struct node
{
int data;
node *next;
}node, *LinkStack;
//创建空栈:
LinkStack CreateNULLStack(LinkStack &S)
{
S = (LinkStack)malloc(sizeof(node)); //申请新结点
if (NULL == S)
{
printf("Fail to malloc a new node.\n")
return NULL;
}
S->data = 0; //初始化新结点
S->next = NULL;
return S;
}
//栈的插入函数:
LinkStack Push(LinkStack &S, int data)
{
if (NULL == S) //检验栈
{
printf("There no node in stack!");
return NULL;
}
LinkStack p = NULL;
p = (LinkStack)malloc(sizeof(node)); //申请新结点
if (NULL == p)
{
printf("Fail to malloc a new node.\n");
return S;
}
if (NULL == S->next)
{
p->next = NULL;
}
else
{
p->next = S->next;
}
p->data = data; //初始化新结点
S->next = p; //插入新结点
return S;
}
//出栈函数:
node Pop(LinkStack &S)
{
node temp;
temp.data = 0;
temp.next = NULL;
if (NULL == S) //检验栈
{
printf("There no node in stack!");
return temp;
}
temp = *S;
10
if (S->next == NULL)
{
printf("The stack is NULL,can't pop!\n");
return temp;
}
LinkStack p = S->next; //节点出栈
S->next = S->next->next;
temp = *p;
free(p);
p = NULL;
return temp;
}
//双栈实现队列的入队函数:
LinkStack StackToQueuPush(LinkStack &S, int data)
{
node n;
LinkStack S1 = NULL;
CreateNULLStack(S1); //创建空栈
while (NULL != S->next) //S 出栈入 S1
{
n = Pop(S);
Push(S1, n.data);
}
Push(S1, data); //新结点入栈
while (NULL != S1->next) //S1 出栈入 S
{
n = Pop(S1);
Push(S, n.data);
}
}
说明:用两个栈能够实现一个队列的功能,那用两个队列能否实现一个队列的功能呢?结果是否定的,因为栈是先进后出,将两个栈连在一起,就是先进先出。而队列是现先进先出,无论多少个连在一起都是先进先出,而无法实现先进后出。
深度的计算函数:
int depth(BiTree T)
{
if(!T) return 0; //判断当前结点是否为叶子结点
int d1= depth(T->lchild); //求当前结点的左孩子树的深度
int d2= depth(T->rchild); //求当前结点的右孩子树的深度
} return (d1>d2?d1:d2)+1;
注意:根据二叉树的结构特点,很多算法都可以用递归算法来实现。
直接插入排序编程实现如下:
#include
void main( void )
{
int ARRAY[10] = { 0, 6, 3, 2, 7, 5, 4, 9, 1, 8 };
int i,j;
for( i = 0; i < 10; i++)
{
cout<<ARRAY[i]<<" ";
}
cout<<endl;
for( i = 2; i <= 10; i++ ) //将 ARRAY[2],…,ARRAY[n]依次按序插入
{
if(ARRAY[i] < ARRAY[i-1]) //如果 ARRAY[i]大于一切有序的数值,
//ARRAY[i]将保持原位不动
{
ARRAY[0] = ARRAY[i]; //将 ARRAY[0]看做是哨兵,是 ARRAY[i]的副本 j = i - 1;
do{ //从右向左在有序区 ARRAY[1..i-1]中 //查找 ARRAY[i]的插入位置
ARRAY[j+1] = ARRAY[j]; //将数值大于 ARRAY[i]记录后移 j-- ;
}while( ARRAY[0] < ARRAY[j] );
ARRAY[j+1]=ARRAY[0]; //ARRAY[i]插入到正确的位置上
}
}
for( i = 0; i < 10; i++)
{
cout<<ARRAY[i]<<" ";
}
cout<<endl;
}
注意:所有为简化边界条件而引入的附加结点(元素)均可称为哨兵。引入哨兵后使得查找循环条件的时间大约减少了一半,对于记录数较大的文件节约的时间就相当可观。类似于排序这样使用频率非常高的算法,要尽可能地减少其运行时间。所以不能把上述算法中的哨兵视为雕虫小技。
注意:在直接选择排序中,具有相同关键码的对象可能会颠倒次序,因而直接选择排序算法是一种不稳定的排序方法。在本例中只是例举了简单的整形数组排序,肯定不会有什么问题。但是在复杂的数据元素序列组合中,只是根据单一的某一个关键值排序,直接选择排序则不保证其稳定性,这是直接选择排序的一个弱点。
堆排序编程实现:
说明:堆排序,虽然实现复杂,但是非常的实用。另外读者可是自己设计实现小堆排序的算法。虽然和大堆排序的实现过程相似,但是却可以加深对堆排序的记忆和理解。
说明:队列为基数排序的实现提供了很大的方便,适当的数据机构可以减少算法的复杂度,让更多的算法实现更容易。
编程规范可总结为:程序的可行性,可读性、可移植性以及可测试性。
说明:这是编程规范的总纲目,面试者不一定要去背诵上面给出的那几个例子,应该去理解这几个例子说明的问题,想一想,自己如何解决可行性、可读性、可移植性以及可测试性这几个问题,结合以上几个例子和自己平时的编程习惯来回答这个问题。
代码一是错的,代码二是正确的。
说明:在数据安全的情况下大类型的数据向小类型的数据转换一定要显示的强制类型转换。
(1)&和|对操作数进行求值运算,&&和||只是判断逻辑关系。(2)&&和||在在判断左侧操作数就能确定结果的情况下就不再对右侧操作数求值。
注意:在编程的时候有些时候将&&或||替换成&或|没有出错,但是其逻辑是错误的,可能会导致不可预想的后果(比如当两个操作数一个是 1 另一个是 2 时。
指针和引用主要有以下区别:
(1) 引用必须被初始化,但是不分配存储空间。指针不声明时初始化,在初始化的时候需要分配存储空间。
(2) 引用初始化以后不能被改变,指针可以改变所指的对象。
(3) 不存在指向空值的引用,但是存在指向空值的指针。
注意:引用作为函数参数时,会引发一定的问题,因为让引用作参数,目的就是想改变这个引用所指向地址的内容,而函数调用时传入的是实参,看不出函数的参数是正常变量,还是引用,因此可能会引发错误。所以使用时一定要小心谨慎。
输入一个整数和一棵二元树。从树的根结点开始往下访问,一直到叶结点所经过的所有结点形成一条路径。打印出和与输入整数相等的所有路径。例如,输入整数 9 和如下二元树:
则打印出两条路径:3,6 和 3,2,4。
注意:数据结构一定要活学活用,例如本题,把所有的结点都压入栈,而不符合条件的结点弹出栈,很容易实现了有效路径的查找。虽然用链表也可以实现,但是用栈更利于理解这个问题,即适当的数据结构为更好的算法设计提供了有利的条件。
写一个“标准”宏 MIN,这个宏输入两个参数并且返回较小的一个。
【答案】
#define min(a,b)((a)<=(b)?(a):(b))
1
注意:在调用时一定要注意这个宏定义的副作用,如下调用:
((++*p)<=(x)?(++*p):(x)
p 指针就自加了两次,违背了 MIN 的本意。
(1) 用法不同:typedef 用来定义一种数据类型的别名,增强程序的可读性。define 主要用来定义常量,以及书写复杂使用频繁的宏。
(2) 执行时间不同:typedef 是编译过程的一部分,有类型检查的功能。define 是宏定义,是预编译的部分,其发生在编译之前,只是简单的进行字符串的替换,不进行类型的检查。
(3) 作用域不同:typedef 有作用域限定。define 不受作用域约束,只要是在 define 声明后的引用都是正确的。
(4) 对指针的操作不同:typedef 和 define 定义的指针时有很大的区别。
注意:typedef 定义是语句,因为句尾要加上分号。而 define 不是语句,千万不能在句尾加分号。
const 用来定义一个只读的变量或对象。主要优点:便于类型检查、同宏定义一样可以方便地进行参数的修改和调整、节省空间,避免不必要的内存分配、可为函数重载提供参考。
说明:const 修饰函数参数,是一种编程规范的要求,便于阅读,一看即知这个参数不能被改变,实现时不易出错。 const修饰成员函数不可修改成员变量。
static 在 C 中主要用于定义全局静态变量、定义局部静态变量、定义静态函数。在 C++中新增了两种作用:定义静态数据成员、静态函数成员。
注意:因为 static 定义的变量分配在静态区,所以其定义的变量的默认值为 0,普通变量的默认值为随机数,在定义指针变量时要特别注意。
extern 标识的变量或者函数声明其定义在别的文件中,提示编译器遇到此变量和函数时在其它模块中寻找其定义。
在程序中,流操作符>>和<<经常连续使用。因此这两个操作符的返回值应该是一个仍旧支持这两个操作符的流引用。其他的数据类型都无法做到这一点。
注意:除了在赋值操作符和流操作符之外的其他的一些操作符中,如+、-、*、/等却千万不能返回引用。因为这四个操作符的对象都是右值,因此,它们必须构造一个对象作为返回值。
指针常量是指定义了一个指针,这个指针的值只能在定义时初始化,其他地方不能改变。常量指针是指定义了一个指针,这个指针指向一个只读的对象,不能通过常量指针来改变这个对象的值。
指针常量强调的是指针的不可改变性,而常量指针强调的是指针对其所指对象的不可改变性。
注意:无论是指针常量还是常量指针,其最大的用途就是作为函数的形式参数,保证实参在被调用函数中的不可改变特性。
请写出以下代码的打印结果:
#include
#include
void main(void)
{
char str[13]=“Hello world!”;
char *pStr=“Hello world!”;
cout<
}
【答案】
打印结果:
13
4
12 12
注意:一定要记得数组名并不是真正意义上的指针,它的内涵要比指针丰富的多。但是当数组名当做参数传递给函数后,其失去原来的含义,变作普通的指针。另外要注意 sizeof 不是函数,只是操作符。
“野指针”产生原因及解决办法如下:
(1) 指针变量声明时没有被初始化。解决办法:指针声明时初始化,可以是具体的地址值,也可让它指向 NULL。
(2) 指针 p 被 free 或者 delete 之后,没有置为 NULL。解决办法:指针指向的内存空间被释放后指针应该指向 NULL。
(3) 指针操作超越了变量的作用范围。解决办法:在变量的作用域结束前释放掉变量的地址空间并且让指针指向 NULL。
注意:“野指针”的解决方法也是编程规范的基本原则,平时使用指针时一定要避免产生“野指针”,在使用指针前一定要检验指针的合法性。
常引用的引入主要是为了避免使用变量的引用时,在不知情的情况下改变变量的值。常引用主要用于定义一个普通变量的只读属性的别名、作为函数的传入形参,避免实参在调用函数中被意外的改变。
说明:很多情况下,需要用常引用做形参,被引用对象等效于常对象,不能在函数中改变实参的值,这样的好处是有较高的易读性和较小的出错率。
编码实现函数 atoi(),设计一个程序,把一个字符串转化为一个整型数值。例如数字:“5486321”,转化成字符:5486321。
【答案】
int myAtoi(const char * str)
{
int num = 0; //保存转换后的数值
int isNegative = 0; //记录字符串中是否有负号
int n = 0;
char *p = str;
if (p == NULL) //判断指针的合法性
{
return -1;
}
while (*p++ != ‘\0’) //计算数字符串度
{
n++;
}
p = str;
if (p[0] == ‘-’) //判断数组是否有负号
{
isNegative = 1;
}
char temp = ‘0’;
for (int i = 0; i < n; i++)
{
char temp = *p++;
if (temp > ‘9’ || temp < ‘0’) //滤除非数字字符
{
continue;
}
if (num != 0 || temp != ‘0’) //滤除字符串开始的 0 字符
{
temp -= 0x30; //将数字字符转换为数值
num += temp *int(pow(10, n - 1 - i));
}
}
if (isNegative) //如果字符串中有负号,将数值取反
{
return (0 - num);
}
else
{
return num; //返回转换后的数值
}
}
注意:此段代码只是实现了十进制字符串到数字的转化,读者可以自己去实现 2 进制,8 进制,10 进制,16 进制的转化。
三者主要有以下不同之处:
(1) 操作对象不同,strcpy 的两个操作对象均为字符串,sprintf 的操作源对象可以是多种数据类型,目的操作对象是字符串,memcpy 的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。
(2) 执行效率不同,memcpy 最高,strcpy 次之,sprintf 的效率最低。
(3) 实现功能不同,strcpy 主要实现字符串变量间的拷贝,sprintf 主要实现其他数据类型格式到字符串的转化,memcpy 主要是内存块间的拷贝。
说明:strcpy、sprintf 与 memcpy 都可以实现拷贝的功能,但是针对的对象不同,根据实际需求,来选择合适的函数实现拷贝功能。
while(1)
{ }
说明:很多种途径都可实现同一种功能,但是不同的方法时间和空间占用度不同,特别是对于嵌入式软件,处理器速度比较慢,存储空间较小,所以时间和空间优势是选择各种方法的首要考虑条件。
面试题 47:编码实现某一变量某位清 0 或置 1
给定一个整型变量 a,写两段代码,第一个设置 a 的 bit 3,第二个清 a 的 bit 3,在以上两个操作中,要保持其他位不变。
【答案】
#define BIT3 (0x1 << 3 )
Satic int a;
//设置 a 的 bit 3:
void set_bit3( void )
{
a |= BIT3;
} //将 a 第 3 位置 1
//清 a 的 bit 3
void set_bit3( void )
{
a &= ~BIT3;
} //将 a 第 3 位清零
1
2
3
4
5
6
7
8
9
10
11
12
说明:在置或清变量或寄存器的某一位时,一定要注意不要影响其他位。所以用加减法是很难实现的。
中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展——让标准 C 支持中断。具体代表事实是,产生了一个新的关键字__interrupt。下面的代码就使用了__interrupt 关键字去定义一个中断服务子程序(ISR),请评论以下这段代码。
__interrupt double compute_area (double radius)
{
double area = PI * radius * radius; printf(" Area = %f", area); return area;
}
1
2
3
4
【答案】
这段中断服务程序主要有以下四个问题:
(1) ISR 不能返回一个值。
(2) ISR 不能传递参数。
(3) 在 ISR 中做浮点运算是不明智的。
(4) printf()经常有重入和性能上的问题。
注意:本题的第三个和第四个问题虽不是考察的重点,但是如果能提到这两点可给面试官留下一个好印象。
构造函数不能是虚函数。而且不能在构造函数中调用虚函数,因为那样实际执行的是父类的对应函数,因为自己还没有构造好。析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必须的。
析构函数也可以是纯虚函数,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。
说明:虚函数的动态绑定特性是实现重载的关键技术,动态绑定根据实际的调用情况查询相应类的虚函数表,调用相应的虚函数。
面向对象可以理解成对待每一个问题,都是首先要确定这个问题由几个部分组成,而每一个部分其实就是一个对象。然后再分别设计这些对象,最后得到整个程序。传统的程序设计多是基于功能的思想来进行考虑和设计的,而面向对象的程序设计则是基于对象的角度来考虑问题。这样做能够使得程序更加的简洁清晰。
说明:编程中接触最多的“面向对象编程技术”仅仅是面向对象技术中的一个组成部分。发挥面向对象技术的优势是一个综合的技术问题,不仅需要面向对象的分析,设计和编程技术,而且需要借助必要的建模和开发工具。
同一进程中的线程独享的资源有栈、线程ID,共享的资源有代码段、数据段、堆、文件描述符、套接字和端口等。
在操作系统中,进程是资源分配的基本单位,而线程则是调度执行的基本单位。线程相较于进程而言,拥有更小的上下文切换代价和更快的创建速度。虽然线程是在进程空间内运行,但它们并不是完全孤立的。以下是详细分析:
线程独享资源:
线程共享资源:
malloc()
或new
操作分配的内存,是进程范围内共享的。总之,理解线程之间共享和独享资源的机制对于多线程编程至关重要。正确地管理共享资源可以避免竞态条件和死锁等问题,而合理地使用独享资源则有助于提高程序的并发性能。
常用的两种动态存储分配办法是栈式动态分配和堆式动态分配。
malloc
、calloc
、realloc
和free
等函数来分配和释放内存。堆的特点是可以按需分配任意大小的内存块,并且生命周期由程序员控制,直到显式地释放内存或者程序结束。总的来说,栈和堆都是程序运行时用于存储数据的内存区域,但它们的管理方式和使用场景有所不同。栈通常用于管理局部变量和函数调用,而堆则用于管理程序运行时动态分配的内存。两者共同构成了程序的动态存储分配机制,为程序的运行提供了灵活性和效率。
常用参数传递方式有按值传递、按引用传递和按指针传递。
在程序设计中,参数传递是函数或方法调用时的一个重要概念,它决定了调用者传递给被调用者的参数值如何被使用和修改。以下是三种常用的参数传递方式:
总的来说,这三种参数传递方式各有用途,选择哪一种取决于具体的应用场景和程序设计的需求。按值传递适用于不需要在函数内部修改参数值的情况;按引用传递和按指针传递则适用于需要在函数内部修改参数值的情况。在设计函数接口时,应该明确参数的传递方式,以确保程序的正确性和可维护性。
指针和引用都是C++中用来间接访问变量或对象的机制,但它们在使用上有着本质的区别。具体来说:
A、引用使用是无需解引用,指针需解引用。引用在使用时直接通过别名来操作对象,不需要额外的解引用操作。而指针存储的是地址,需要通过解引用操作(*ptr)来访问或修改指向的内存内容。
B、引用不能为空,指针可以为空。由于引用是变量或对象的别名,它必须被初始化并且一直引用同一个对象。而指针可以指向任何地址,包括NULL(或在现代C++中的nullptr),表示不指向任何对象。
C、引用在定义时被初始化一次,之后不可变;指针指向的值和本身的值是可变的。引用在声明时必须初始化,并且一旦与某个变量绑定,就不能再改变引用的目标。指针则可以在任何时候改变其指向的地址,也可以改变指向地址上的值。
综上所述,指针提供了更多的灵活性,但也带来了更多的责任,如需要管理内存和处理空指针等。而引用则提供了一种安全的方式来间接访问对象,但不适用于需要改变引用目标的情况。在实际编程中,应根据具体需求选择合适的机制。
vector是C++标准库中提供的一种动态数组容器,它具备以下特点:
综上所述,关于vector的正确说法是C,即vector可以实现随机访问。这是因为vector内部的元素在内存中是连续存储的,使得它能够高效地支持随机访问操作。在实际使用中,这种特性使得vector成为处理大量数据时的首选容器之一。
List是C++标准模板库(STL)中的双向链表容器,它提供了快速的插入和删除操作,尤其是在列表的中间或开头插入元素时,比vector效率更高。现在来分析每个选项:
A、list 可以实现随机访问:这是不正确的。由于list是基于节点的双向链表结构,它不支持随机访问,因为访问任何元素都需要从头部或尾部开始逐个遍历节点,直到到达目标位置。 B、list随机插入的效率与顺序插入的效率一样:这通常是正确的。list的插入操作不会像vector那样导致内存拷贝,因为它只需要修改指向元素的指针。无论是在列表的开头、中间还是末尾插入,list都能保持较高的效率。 C、list在内存中是连续的:这是不正确的。list在内存中是非连续的,因为它由分散的节点组成,每个节点包含数据和指向前后节点的指针。 D、list 不能从后向前遍历:这是不正确的。list支持双向遍历,可以从头部向尾部遍历,也可以从尾部向头部遍历。
综上所述,list是一个基于节点的双向链表容器,它不支持随机访问,但提供了高效的插入和删除操作,且能够从尾部向头部进行遍历。在实际应用中,选择list还是其他容器类型,应根据具体的需求和操作来决定。
Map和unordered_map都是C++标准库中的关联容器,它们存储的是键值对(key-value pairs),但它们的底层实现和数据组织方式有所不同。具体来说:
综上所述,map确保了数据的有序性,而unordered_map提供了基于哈希表的快速访问。在使用这些容器时,应根据具体需求选择合适的类型,例如,如果需要保持元素的排序,则应使用map;如果需要快速的查找性能,则可以考虑使用unordered_map。同时,要注意在操作这些容器时,对迭代器的有效性进行适当的管理。
从vector头部删除一个数据,vector会发生以下变化:
A、后面的数据集体向前移动
当从vector的头部删除数据时,所有后面的元素都会向前移动一位,以填补被删除元素留下的空位。这是为了保持vector中元素的连续性。因此,选项A是正确的。
其他选项的描述不准确:
综上所述,正确答案是A,即从vector头部删除一个数据时,后面的数据会集体向前移动。
当vector.size() == vector.capacity()
时,以下说法正确的是:
A、继续插入数据,vector需要重新申请内存空间
这个描述是正确的。当一个vector
的size()
(当前元素个数)等于它的capacity()
(容器在不重新分配内存的情况下能够容纳的最大元素数量)时,意味着它已经用完了所有预先分配的内存空间。如果此时继续插入新的元素,vector
将需要重新申请一块更大的内存空间来容纳这些额外的元素,并且将现有元素复制到新的内存位置。
其他选项的描述情况如下:
vector
的内存占用比存储相同数据量的list
大:这个描述通常是错误的。vector
和list
有不同的内存使用特性。vector
在内存中是连续存储的,而list
是基于节点的双向链表结构,每个元素都包装在一个节点中,每个节点还包含指向前后元素的指针。因此,list
通常会由于额外的指针信息而有更大的内存开销。然而,如果考虑到内存分配的粒度和可能的内部碎片,在某些情况下vector
可能会比list
有稍微更多的内存占用。vector
的内存占用小于存储相同数据量的数组:这个描述通常是错误的。静态数组在存储相同数据量时通常具有更小的内存开销,因为它没有额外的结构信息或管理数据的开销。但是,静态数组的大小是固定的,而vector
可以动态增长。综上所述,正确答案是A,即当vector.size() == vector.capacity()
时,如果继续插入数据,vector
需要重新申请内存空间。
关于new/delete和malloc/free的问题,正确答案是C、使用new会构造对象。
具体分析如下:
new
操作符返回指向新创建对象的指针,这个指针的类型是对象类型的*,而不是void*
。delete
表达式需要一个与new
表达式相同类型的指针,也不是void*
。new/delete
和malloc/free
有一些重要的区别。特别是,new
会调用对象的构造函数而malloc
不会,反之,delete
会调用析构函数而free
不会。混用可能导致对象的构造函数或析构函数不被正确调用。new
不仅分配内存,还负责调用对象的构造函数来初始化对象。这是它与malloc
的一个重要区别。delete
本身不能重载,但是可以重载全局的或类的operator delete
来自定义释放内存的行为。综上所述,new和delete操作的对象不是void*,它们操作的是具体类型的对象。new/delete和malloc/free不应该混用,因为这两者在处理对象的构造和析构上有不同的行为。使用new时会涉及到对象的构造过程,而delete则与对象的析构有关。最后,delete本身是不能重载的,但可以通过重载operator delete来自定义删除对象时的行为。
关于shared_ptr
的正确说法是:
C、shared_ptr
使用的是引用计数法。
shared_ptr
在C++中是一种智能指针,用于自动管理对象的生命周期,主要通过引用计数机制来避免内存泄漏。每当一个新的shared_ptr
实例被创建并指向一个对象时,该对象的引用计数就会增加。当shared_ptr
被销毁或指向另一个对象时,原对象的引用计数会减少。一旦引用计数变为0,即没有任何shared_ptr
实例指向该对象时,该对象就会被自动删除。
然而,shared_ptr
确实存在循环引用的问题。如果两个或多个对象通过shared_ptr
互相引用,形成闭环,那么这些对象的引用计数永远不会降至0,从而导致内存无法被释放。这就是所谓的循环引用问题。
此外,shared_ptr
是可以自定义删除器(deletor)的,这允许用户定义特定的内存释放逻辑,以便在shared_ptr
自动管理的对象不再使用时执行特定操作。因此,选项B是错误的。
总的来说,shared_ptr
通过引用计数法来管理对象的生命周期,但需要开发者注意避免循环引用的情况,以免造成内存泄漏。同时,它也提供了删除器的定制功能,以适应不同的资源管理需求。
在C++中,可能导致内存泄漏的原因包括:
A、忘记调用delete
或free
:如果动态分配的内存没有被释放,那么这块内存就会一直被占用,导致内存泄漏。
B、未定义拷贝构造函数或未重载赋值运算符:如果类中有动态分配的资源,并且没有正确地定义拷贝构造函数和赋值运算符,那么在复制对象时可能会导致原始对象的资源被错误地释放,或者新对象的资源没有被正确分配,从而引发内存泄漏。
C、没有将基类的析构函数定义为虚函数:如果基类有一个析构函数,并且派生类通过继承创建了对象,当使用基类的指针删除派生类的对象时,如果没有将基类的析构函数定义为虚函数,那么派生类的部分析构函数将不会被调用,可能导致派生类中的资源没有被正确释放,从而引发内存泄漏。
D、栈上面的对象没有手动释放:栈上的对象在超出作用域时会自动被销毁,不需要手动释放。因此,这个选项并不是导致内存泄漏的原因。
unique_ptr
是C++中的一种独占所有权的智能指针,它确保在任何时刻只有一个对象可以拥有指向资源的所有权。由于这种独占性质,unique_ptr
不允许拷贝构造,但支持移动语义,即所有权可以通过移动操作转移给另一个unique_ptr
。这样,它就保证了资源的唯一所有权,避免了多个对象同时释放同一资源导致的程序错误。
选项C是错误的,因为shared_ptr
和unique_ptr
是两种不同的智能指针,它们有不同的用途和实现机制。shared_ptr
允许多个指针共享同一资源,而unique_ptr
则不允许。因此,不能直接由shared_ptr
构造unique_ptr
,但可以通过std::move
将unique_ptr
的所有权转移到shared_ptr
,或者反之。
用户程序转变为可执行程序时,不需要经历翻译步骤。
用户程序到可执行程序的转变通常包括以下几个步骤:
值得注意的是,在上述过程中,并没有单独的“翻译”步骤。虽然可以认为汇编过程是一种翻译过程,因为它将汇编语言翻译成机器语言,但在程序从源代码到可执行文件的转换过程中,通常不将其称为“翻译”步骤。因此,选项B“翻译”不属于转变过程中的必要步骤。
两个字节的数据 0x12 0x34 的小端序存储方式表示为 0x34 0x12。
小端序(Little-Endian)是一种内存存储方式,它将多字节数据的低位字节存放在起始地址,高位字节存放在结束地址。具体到这个问题中,数据0x12和0x34以小端序存储时,低位字节0x34会被放置在低地址端,而高位字节0x12则放在高地址端。这种存储方式与大端序(Big-Endian)相反,后者是将高位字节存放在低地址端,低位字节存放在高地址端。
了解小端序和大端序的概念对于理解不同计算机系统之间如何交换数据是非常重要的,尤其是在进行网络通信或者跨平台软件开发的时候。不同的硬件架构可能采用不同的字节序,因此确保数据的正确解释和处理是必要的。
解析:
1. `char str[] = "hello world!"`: 在C语言中,字符串的sizeof计算的是分配给该字符数组的空间大小,也就是字符的数量(不包括最后的'\0')。所以,"hello world!"有12个字符加上一个'\0',共13个字符。因此,`sizeof(str)`的值是13。
2. `char *p = str;`: 这是一个字符指针,无论它指向的字符串有多长,sizeof都会返回指针的大小。在32位系统中,指针的大小为4字节。因此,`sizeof(p)`的值是4。
3. `int n = 10;`: sizeof(int)在大多数系统中都是4字节,包括32位系统。因此,`sizeof(n)`的值是4。
4. `void Foo (char str[100]){}`: 在函数内部,数组参数被视为指针,所以`sizeof(str)`将返回指针的大小,即4字节。
5. `void *p = malloc(100);`: 这是动态分配内存的指针,其大小也是指针的大小,即4字节。
6. `struct A {...}`: 结构体的大小是所有成员大小之和,再加上可能的填充。Windows和Linux的内存对齐规则可能不同,所以在这两种系统中,结构体的大小可能不同。具体的大小需要根据各个成员的类型和系统进行计算。
代码:
```c
#include
int main() {
char str[] = "hello world!";
char *p = str;
int n = 10;
printf("sizeof(str) = %lu
", sizeof(str)); // 输出:13
printf("sizeof(p) = %lu
", sizeof(p)); // 输出:4
printf("sizeof(n) = %lu
", sizeof(n)); // 输出:4
void Foo(char str[100]) {
printf("sizeof(str) = %lu
", sizeof(str)); // 输出:4
}
void *p = malloc(100);
printf("sizeof(p) = %lu
", sizeof(p)); // 输出:4
struct A {
char a;
int b;
int c;
long long d;
char e;
};
printf("32位windows系统中sizeof(A) = %lu
", sizeof(struct A));
printf("32位linux系统中sizeof(A) = %lu
", sizeof(struct A));
return 0;
}
```
注意:这段代码中的sizeof(struct A)的值取决于你的编译器和操作系统。你需要在你的系统上运行这段代码来获取准确的结果。
唯一区别在于变量的默认访问权限不一样
class默认访问权限为私有
struct默认访问权限为公开
构造函数 方法名与类名一致
没有返回值,不用写void
构造函数可以有参数,可以重载
创建对象的时候调用,且只调用一次
析构函数 方法名与类名一致,在名称前需要加~
没有返回值,不用写void
不允许有参数,不能重载
在对象销毁前调用,且调用一次
构造函数和析构函数都必须有,如果我们不提供,编译器会提供一个空的
XX公司的题。
一、基本问题(80%)
1、const、static作用。
2、c++面向对象三大特征及对他们的理解,引出多态实现原理、动态绑定、菱形继承。
3、虚析构的必要性,引出内存泄漏,虚函数和普通成员函数的储存位置,虚函数表、虚函数表指针。
4、malloc、free和new、delete区别,引出malloc申请大内存、malloc申请空间失败怎么办。
5、stl熟悉吗,vector、map、list、hashMap,vector底层,map引出红黑树。优先队列用过吗,使用的场景。无锁队列听说过吗,原理是什么(比较并交换)
6、实现擅长的排序,说出原理(快排、堆排)
7、四种cast,智能指针
8、tcp和udp区别
9、进程和线程区别。
10、指针和引用作用以及区别。
11、c++11用过哪些特性,auto作为返回值和模板一起怎么用,函数指针能和auto混用吗。
12、boost用过哪些类,thread、asio、signal、bind、function
13、单例、工厂模式、代理、适配器、模板,使用场景。
14、QT信号槽实现机制,QT内存管理,MFC消息机制。
15、进程间通信。会选一个详细问。
16、多线程,锁和信号量,互斥和同步。
17、动态库和静态库的区别。
//auto作为返回值和模板一起怎么用,函数指针能和auto混用吗
#include
using namespace std;
template
auto add(T t,U u) -> decltype(t+u)
{
return t+u;
}
template
auto sub(T t,U u) -> decltype(t-u)
{
return t-u;
}
template
auto pro(T t,U u) -> decltype(t*u)
{
return t*u;
}
template
auto div(T t,U u) -> decltype(t/u)
{
try
{
return t/u;
}
catch(...)
{
exit(0);
}
}
int main()
{
int x = 520;
double y= 13.14;
auto z = add(x,y);
cout<
二、保留问题(20%)
1、提高c++性能,你用过哪些方式去提升(构造、析构、返回值优化、临时对象(使用operator=()消除临时对象)、内联(内联技巧、条件内联、递归内联、静态局部变量内联)、内存池、使用函数对象不使用函数指针、编码(编译器优化、预先计算)、设计(延迟计算、高效数据结构)、系统体系结构(寄存器、缓存、上下文切换))。
2、编译原理,尝试自己写过语言或语言编译器。
3、泛型模板实用度高。
4、对多种计算机语言熟悉。
5、Git项目了解多。
6、针对网络框架(DPDK)、逆向工程(汇编)、分布式集群(docker、k8s、redis等)、CPU计算(nvidia cuda)、图像识别(opencv、opengl、tensorflow等)、AI等有研究。