class A1
{
public:
A1(){}
void func();
~A1(){}
};
class B1:public A1
{
public:
B1(){}
void func();
~B1(){}
};
class A2
{
public:
A1(){}
virtual ~A1(){}
};
class B2:public A2
{
public:
B1(){}
~B1(){}
};
这个经常遇到也是最基础的。
对于直接定义而非动态申请,即不是通过new和指针来操作,那么这种情况,无论有没有用virtual修饰符,都会调用到正确的对应于类型的函数,并且子类的定义到回收释放经过正常的基构->子构->子析->基构过程,与是否virtual修饰无关。
对于通过动态申请并由指针操作,那么
{
A1 *pA1 = new B1;
pA1->func();//无virtual,调用类A1的func
delete pA1;//基构->子构->基构,无virtual,无法正确调用到子析
B1 *pB1 = new B1;
pB1->func();//调用类B1的func
delete pB1;//基构->子构->子析->基构,这种情况是会正常调用的。
}
{
A2 *pA2 = new B2;
pA2->func();//调用类B2的func
delete pA2;//基构->子构->子析->基构
}
所以对于存在继承关系的类,父类的析构函数最好定义为虚函数(virtual),这样在释放子类内存的时候,可以正确调用到子类的析构函数。
题外话,构造函数不可以声明为virtual。
3. 在C++中,类的成员变量和成员函数默认是private的(不要忘记写public。。),而结构体的成员变量默认是public的。
4. 多重继承,继承时要在类前加virtual修饰符,否则编译不通过,这样可以使得子类只有顶级父类的一份拷贝。(gcc测试)
5. printf(“abc”)函数的返回值是3,返回输出的字符数,无论输出的是什么数据类型。
6. 线性窥孔/窥孔优化:是一种指令优化技术,优化对象可以是中间代码,也可以是目标代码;每次只处理一组相邻的指令,以线性方式扫描。
7. volatile关键字:提醒编译器它后面所定义的变量随时可能改变,因此,编译后的程序每次需要存储或者读取这个变量时,都直接从内存地址读取和写入数据,而不是从寄存器中。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,导致这个变量被别的线程更新而出现不一致。(用于多线程)。
8. const_cast运算符:用于修改类型的const或volatile属性。
{
const int x = 10;
char num1[x];
int *p = const_cast<int *>(&x);
*p = 15;
char num2[x];
char num3[*p]; //这个在C99之后可以编译通过,而在VC中是编译不通过的,具体会再开问说明
cout << "size of num1 " << sizeof(num1) << endl;
cout << "size of num2 " << sizeof(num2) << endl;
cout << "size of num3 " << sizeof(num3) << endl;
cout << "x " << &x << ": " << x << endl;
cout << "p " << p << ": " << *p << endl;
}
以上代码运行结果如下:
这个问题在金山WPS面试的时候被面试官问及,const修饰的变量的值可以改变吗?因为之前用上述代码测试过,x的值仍然为10,而且在别人博客看到对于x和*p的输出不一样的解释是“不可预料的结果”。所以很自信地说就算使用const_cast也无法修改。
面试官笑了一笑,这个变量的值是不是存在内存里,既然在内存里为什么不能修改?
结果是面试官是对的,回想起来真是挺尴尬,不过也感谢这位面试官,在面试过程中多多少少学到了一些思想,用计算机的原理去想问题,还有对别人的结论不能完全相信。
{
volatile const int x = 10;
//题外话
//原本const声明的变量(常量)是可以用来作为数组定义的大小的,
//但用volatile修饰之后,在不支持变量长度数组(VLA)的标准中就会编译报错
char num1[x];//C99标准的编译器可以通过
int *p = const_cast<int *>(&x);
*p = 15;
char num2[x];
cout << "size of num1 " << sizeof(num1) << endl;
cout << "size of num2 " << sizeof(num2) << endl;
cout << &x << ": " << x << endl;
cout << (int *)&x << ": " << x << endl;
cout << p << ": " << *p << endl;
}
其实问题的答案很简单,修改一下验证程序代码。
volatile修饰词的作用上面已经提及,这里为什么要用到它呢?先解释上面x和*p结果不一致问题:编译器会优化读取,导致x的值是从寄存器中取出的,而*p是在内存中取得。所以呢,实际上const的值在内存中还是修改成功了的。
顺带一提,输出第3行的&x的结果为什么为1?因为cout的<<操作符没有对volatile int *类型进行重载,导致被强制转化为bool型,输出为1。
9. C++另外几个关于强制转换的运算符。
- reinterpret_cast:可用于无关类型的转换,直接拷贝比特位。例如将指针和足够大的整数之间的转换,函数指针的转换,对象指针的转换等。
- static_cast:只能用于有关类型(例如继承)之间的转换。子类指针和父类指针之间的转换都是可行的,但子类指针转父类指针是安全正确的,父类指针转子类指针则不一定安全正确。这个运算符并不常用于指针或引用的转换,更多是用于基础类型或对象的转换上。
- dynamic_cast:也只能用于有关类型之间,基于类对象的指针或引用之间的转换。
static_cast | dynamic_cast | |
---|---|---|
子类指针转父类指针 | 编译成功且正确 | 编译成功且正确 |
父类指针转子类指针 | 编译成功但是不安全 | 编译成功但返回空指针 |
无关类指针间的转换 | 编译不通过 | 编译成功但返回空指针 |
10. 左移运算符和右移运算符 。
一个有符号整型在循环移位的过程中:
正数 | 负数 | 补位 | |
---|---|---|---|
<< (相当于*2) | 过程有正有负,最终为0 | 过程有正有负,最终为0 | 补0 |
>> (相当于/2) | 始终非负,最终为0 | 始终负数,最终为-1 | 正补0,负补1 |
所以在以移位操作为循环/判断条件时,要注意正负数,避免死循环。
11. 异常中不可以再抛出异常,会使程序终止。
12. 对于include的文件,在vs下产生.obj文件。
13. 类模板。
写法 | 正误 |
---|---|
template |
错误,不可重名 |
template |
错误,每个替代符都要跟随一个修饰符 |
template |
正确 |
template |
正确,class A相当于只是一个声明 |
14. sizeof()函数。
对于基本数据类型(32位):
基本数据类型 | sizeof()返回值 |
---|---|
指针(包括 void*),int,long,float | 4 |
char,bool | 1 |
short | 2 |
long long,double | 8 |
long double | 12 |
对于数组:
void func(int num[])
{
int test[10];
int *p = test;
cout << sizeof(test) << endl;//输出40
cout << sizeof(p) << endl;//输出4
cout << sizeof(num) << endl;//输出4
}
//对于一个{}区块内定义的数组,sizeof(数组名)会输出数组实际的大小。
//而对于函数参数传入的num[]虽然声明为数组,但是也是以指针来看待。
对于结构体:
struct A{};
cout << sizeof(A) endl; //空结构体输出1
struct B{
char [5];
};
cout << sizeof(B) << endl; //输出5
struct C{
long long k;
char ch;
};
cout << sizeof(C) << endl; //输出16
//其实不难看出,就是以结构体中最大的数据类型为基数,来进行填坑。
//例如:
strcut D{
char c;
long long k;
char s[9];
};
cout << sizeof(D) << endl; //输出32
//结构体D最大的数据类型为8个字节,那么就以8个字节来填坑。
//|0|1|2|3|4|5|6|7|8|
//|c|-|-|-|-|-|-|-|-|
//| k |
//| s[0]-s[7] |
//|s[8]|-|-|-|-|-|-|-|-|
对于类和对象:
class A{};
//空类(其实还有隐藏的构造函数等),sizeof(A)返回1.
class B{
int func(){int y = 8; return y;}
};
//只有函数的类,sizeof(B)返回1.
class C{
virtual ~C(){}
virtual int func(){}
};
//对于存在虚函数的类(无论多少个虚函数),那么这个类就有对应的一个虚表。那么这个类的对象就相应地拥有一个指向虚表的指针。那么sizeof(C)返回4。
class D{
int a;
long long b;
int func(){}
};
//对于存在成员变量的但不存在虚函数的,类中的函数在对象中不占空间,sizeof()的返回值和结构体一样计算,
class E{
long long b;
int a;
virtual int func(){}
};
//对于存在成员变量的且存在虚函数的类,要考虑到对象中有一个虚表指针4个字节,并且始终在对象的内存空间的最前面。其余和结构体中一样计算,所以此例中sizeof返回值为24.
对于空结构体和空类(或者说没有成员变量的类)中为什么sizeof()返回1?
这是在定义之后,系统会分配一个独一无二的地址,可以称之为占位符吧。测试时也确实如此,每个对象或者变量的地址都是唯一的。
15. 类中的(public属性)静态成员函数/静态成员变量是否可以被继承?
答案是可以的。
16. 那么静态成员函数是否可以用virtual修饰?
答案是不可以的。静态成员函数是与类挂钩的,而virtual是与对象挂钩的,并且通过函数里隐藏传递的this指针形参实现正确调用,而静态成员函数中是不存在这个指针,所以同时用virtual和static修饰函数是矛盾且不可行的。
17. 一个程序的内存分为:代码区,全局/静态区,堆区,栈区。
18. 对释放的内存再操作的影响。
1) new和malloc得到的内存是在堆上的,delete和free之后,再进行读取,程序不会报错终止,但读取到的是脏数据。若再进行写入,内存里的内容会被改写,但程序也随之报错终止。
2) 函数中char *p = "abcde";return p;
这种方式定义的字符串是存放在常量区的,返回后再对指针读取,可以读到正确的内容。但是这种方式定义的字符串,无论在函数中还是函数外,如果企图修改字符串的值,程序都会报错终止。
3) 函数中char p[] = "abcde";return p;
这种方式定义的字符串是在栈上申请内存的。对返回的指针再进行读取,程序不会报错终止,但读取到的是脏数据。若再进行写入,程序直接报错终止。
4)在1)和3)中,堆区释放后写入是会被改写的并报错终止,而栈区是直接报错。这是一个进程对应一个堆区,程序仍然可对该内存改写,而栈区一旦生命周期结束就回收了。
19. int *p[5]
和int (*p)[5]
的区别
int *p[5]
是定义了有一个指针数组,5个指向int型的指针的数组。
int (*p)[5]
是定义了一个二维数组指针,且二维数组的第二维的维度须为5,也就是定义了一个指向[][5]整型数组的指针。
20. 函数指针。
int (*func)(int)
定义了一个函数指针。
typedef int (*func)(int)
func是一个函数指针类型。
21. 运算符优先级。
运算符优先级图片来自http://blog.csdn.net/xiaoxiansweety/article/details/11892009
22. 进程间的通信方式。
管道/有名管道 Pipe/Named pipe;
信号 Signal;
信号量 Semaphore;
共享内存 Share memory;
消息队列 Message Queue;
套接字 Socket.
摘自http://www.cnblogs.com/mydomain/archive/2010/09/23/1833369.html
管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
23.程序设计要求高内聚低耦合。
耦合性从高到低为:内容>公共>外部>控制>标记>数据>非直接。
24.STL中底层实现。
容器 | 底层实现 |
---|---|
vector | 数组 |
list | 双向!!环形链表 |
deque | 双端队列 |
stack/queue | list或deque |
priority_queue | vector和heap |
set | 红黑树,有序,不重复 |
multiset | 红黑树,有序,可重复 |
map | 红黑树,有序,不重复 |
multimap | 红黑树,有序,可重复 |
hash_set | hash表,无序,不重复 |
hash_multiset | hash表,无序,可重复 |
hash_map | hash表,无序,不重复 |
hash_multimap | hash表,无序,可重复 |
25.vector的reserve()和resize()方法。
vector动态增加大小,并不是在原空间之后持续增加新空间,而是以原大小的两倍另外配置一块较大的空间,然后将原内容拷贝过来,之后构造新元素,并释放原空间。因此,对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了。
vector::reserve()对应vector::capacity(),相当于预申请一个内存大小。可以直接使用。
vector::resize()对应vector::size(),表示实际的元素个数。
reserver()只能设置为比当前capacity()值更大或想得的值,否则无效。此外,当设置reserve(3)之后,如果之后空间不够且没有显示地调用reserve(),那么就会以3的2倍的大小申请内存,即6。此时capacity()的值变为6。size()还是实际的元素个数。
resize()可以设置比当前size()值小的数字,那么多余的那部分会被析构掉,size()值变为设置值,capacity()值不变。
resize()也可以设置比当前size()值大而比当前capacity()小的数字,那么会构造出超出的部分,size()值变为设置值,capacity()值不变。
resize()也可以设置比capacity()大的数字,那么会构造出超出的部分,size()值变为设置值,capacity()值的变化就有点复杂了,它不是像之前那样简单的原capacity()值的2倍,而是“原size()值的2倍>现size()值?原size()值的2倍:现size()值“。
26.const char ptr,char const ptr, char * const ptr的区别。
const char * ptr,是一个指向常量字符串的指针。
char const * ptr,同上,两者是等价的。
char * const ptr,是一个指向字符串的常量指针。
27.const的函数返回类型。
主要是针对引用类型的返回值。
1).用const修饰函数引用类型的返回值,可以确保函数不会被当作左值进行赋值操作。
例如int& min (const int &i,const int &j);对于这样的声明,min(a,b)=5不会被报错。
而const int& min(const int &i,const int &j),min(a,b)=5不被允许。
2).对const int& min(const int &i,const int &j)返回的值,可以用int变量接收并且可改(其实是赋值了),但没法用int&变量接收(引用是个别名,类型不匹配),得用const int&变量接受并且不可改。
3)形参里的引用类型不需修改的话最好也加上const,这样可以用常量传参。
1.MBR 主引导记录,VBR 卷引导记录。
来自百度百科
MBR,即主引导记录,是对IBM兼容机的硬盘或者可移动磁盘分区时,在驱动器最前端的一段引导扇区。
MBR描述了逻辑分区的信息,包含文件系统以及组织方式。此外,MBR还包含计算机在启动的第二阶段加载操作系统的可执行代码或连接每个分区的引导记录(VBR)。这个MBR代码通常被称为引导程序。
个人理解一个磁盘对应一个MBR,一个MBR可以对应多个VBR。
2.虚拟内存页式管理:页面替换策略。
FIFO:先进先出。
OPT:最佳页面替换算法,是一种全局页面替换策略
来自百度百科
当要调入一页而必须淘汰旧页时,应该淘汰以后不再访问的页,或距现在最长时间后要访问的页面。它所产生的缺页数最少,然而,却需要预测程序的页面引用串,这是无法预知的,不可能对程序的运行过程做出精确的断言,不过此理论算法可用作衡量各种具体算法的标准。
LRU:近期最少使用算法。
NRU:最近不使用算法。
来自百度百科
当一存储块中的页面访问时,其相应的“页面访问”位由硬件自动置“1”,而由页面管理体制软件周期性地(设周期为T,其值通常为几百毫秒),把所有的页面访问位重新置为“0”。这样,在时间T内,某些被访问的页面,其对应的访问位为“1”而未访问的页面,其对应的访问位为“0”。查寻页面访问位为“0”的页面。在查找过程中,那些被访问的页所对应的访问位被重新置为“0”。由此可见,实际上这种近似LRU算法,已经退化成一种“最近不用”的算法NRU。
3.磁盘调度
FCFS:先来先服务,是一种非抢占式策略。
SSTF:最短寻道时间优先,该算法选择调度处理的磁道是与当前磁头所在磁道距离最近的磁道,以使每次的寻找时间最短。当然,总是选择最小寻找时间并不能保证平均寻找时间最小,但是能提供比FCFS算法更好的性能。这种算法会产生“饥饿”现象。
SCAN:扫描算法/电梯算法,该算法在磁头当前移动方向上选择与当前磁头所在磁道距离最近的请求作为下一次服务的对象,SCAN算法对最近扫描过的区域不公平,因此,它在访问局部性方面不如FCFS算法和SSTF算法好。
C-SCAN:循环扫描算法,在扫描算法的基础上规定磁头单向移动来提供服务,回返时直接快速移动至起始端而不服务任何请求。
4.中断机制:程序直接控制、中断驱动、DMA方式。
1.基于UDP:NFS,SNMP,DNS,TFTP,DHCP等。
2.基于TCP:
FTP(20,21),SSH(22),Telnet(23),SMTP(25),POP3(110),HTTP(80),HTTPS(443)等。
3.HTTP应答码:200 OK,202 Accepted,400 Bad Request,404 Not Found等。
4.SNMP组成:管理信息库MIB、管理信息结构SMI,协议本身。
5.Nagle算法:任意时刻,最多只能有一个未被确认的小段。 所谓“小段”,指的是小于MSS尺寸的数据块,所谓“未被确认”,是指一个数据块发送出去后,没有收到对方发送的ACK确认该数据已收到。
1.设计范式
范式 | 目的 |
---|---|
1NF | 每一列都是单属性 |
2NF | 解决局部依赖,要求实体的属性完全依赖于主关键字 |
3NF | 解决传递依赖,消除了非主属性对键的传递函数依赖 |
BCNF | 消除主属性对键的部分与传递依赖 |
2.SQL语句基础。
作用 | SQL语句 |
---|---|
创建数据库 | CREATE DATABASE <数据库名> |
删除数据库 | DROP DATABASE <数据库名> |
创建表 | CREATE TABLE <表名>(<列名 类型 完整性约束>,...) |
修改表-添加列 | ALTER TABLE <表名> ADD <列名 类型> |
修改表-删除列 | ALTER TABLE <表名> DROP COLUMN <列名> |
修改表-改类型 | ALTER TABLE <表名> MODIFY <列名 类型> |
删除表 | DROP TABLE <表名> |
创建索引 | CREATE [UNIQUE] INDEX <索引名> ON <表名>(<列名序列>) |
删除索引 | DROP INDEX <索引名> ON <表名> |
创建视图 | CREATE VIEW <视图名>(<列名序列>) AS |
删除视图 | DROP VIEW <视图名> |
数据查询 | SELECT <列名或列表达式序列> FROM <表或视图> [WHERE <行表达式>] [GROUP BY <列名序列>] [HAVING <组条件表达式>] [ORDER BY <列名 [ASC|DESC]>,...] |
数据插入 | INSERT INTO <基本表名>[(<列名序列>)] VALUES(<元祖值>) |
数据插入 | INSERT INTO <基本表名>[(<列名序列>)] VALUES(<元祖值>)(<元祖值>),...(<元祖值>) |
数据插入 | INSERT INTO <基本表名>[(<列名序列>)] |
数据插入 | INSERT INTO <基本表名>[(<列名序列>)] TABLE <基本表名2> |
数据删除 | DELETE FROM <基本表名> [WHERE <条件表达式>] |
数据修改 | UPDATE <基本表名> SET <列名>=<值表达式>[,...] [WHERE <条件表达式>] |
数据修改 | UPDATE <基本表名> ROW=(<元祖>) [WHERE <条件表达式>] |
3.全文搜索。
FULLTEXT可以在 CREATE TABLE 时或之后使用 ALTER TABLE 或 CREATE INDEX 在 CHAR、VARCHAR 或 TEXT 列上创建。全文搜索通过 MATCH() 函数完成。
FULLTEXT(字段1,…)
SELECT <表字段,…> FROM <表名> WHERE MATCH(<定义的全文搜索表字段>) AGAINST(‘<搜索字符串>’)
MATCH会在全文搜索的各个列中匹配AGAINST指定的字符串,并且不区分大小写。
MATCH还可以得出搜索字符串在指定列中的相似度,因为MATCH() 会返回一个相关性值。
SELECT <字段,…,MATCH (<定义的全文搜索表字段>) AGAINST(‘<搜索字符串>’) FROM <表名>
4.存储过程。一组为了完成特定功能的SQL 语句集,存储在数据库中,经过第一次编译后再次调用不需要再次编译,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。
格式:
CREATE PROCEDURE <存储过程名>( <[IN|OUT|INOUT]参数名 类型>,…)
begin
…[SQL或结构语句,类似程序语言,IF,WHILE,FOR]
end
例如:
CREATE PROCEDURE lizi (IN shuru INT, OUT shuchu INT )
BEGIN
SELECT COUNT(*) INTO shuchu
FROM biao
WHERE id=shuru ;
END
调用存储过程:CALL <存储过程名>
5.触发器。与表事件相关的特殊的存储过程,它的执行不是由程序调用,也不是手工启动,而是由事件来触发,比如当对一个表进行操作( insert,delete, update)时就会激活它执行。
触发器的四个要素:唯一的名字,关联的表,响应的活动,执行的时机。
格式:
CREATE TRIGGER <触发器名>
< [ BEFORE | AFTER ] > < [ INSERT | UPDATE | DELETE ] >
ON <表名>
FOR EACH ROW
BEGIN
–do something
//这里有2个过渡变量NEW和OLD。对于INSERT语句,只有NEW是合法的;对于DELETE语句,只有OLD才合法;而UPDATE语句可以在和NEW以及OLD同时使用
END
6.事务。是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。 事务处理可以确保除非事务性单元内的所有操作都成功完成,否则不会永久更新面向数据的资源。
事务的4个属性(ACID):原子性,一致性,隔离性,持久性。
BEGIN TRANSACTION //开始事务
//do something
SAVEPOINT <保留点名字>
//do something
ROLLBACK <保留点名字> //回滚到保留点的位置,事务未结束
ROLLBACK/COMMIT //回滚或者提交
7.数据库的索引。是基于B树或其变种B+树实现的。
可用于全值匹配,匹配最左列前缀,匹配某列前缀但要精确匹配前面的列,匹配最左列范围值,匹配某列的范围值但要精确匹配前面的列。
同时,如果不是按照索引最左列开始的顺序进行查找,那么索引不起作用,如果跳过索引中的某一列进行查找,索引也不起作用,如果某个列进行了范围查找,那么它右边的列就无法使用索引优化查找。
8.正则表达式。通常被用来检索、替换那些符合某个模式(规则)的文本。
元字符 | 描述 |
---|---|
\ | 转义字符 |
^ | 匹配输入字符串的开始位置 |
$ | 匹配输入字符串的结束位置 |
* | 匹配前面的子表达式任意次 |
+ | 匹配前面的子表达式一次或多次(大于等于1次) |
? | 匹配前面的子表达式零次或一次。例如,“do(es)?”可以匹配“do”或“does”中的“do”。?等价于{0,1} |
{n} | n是一个非负整数 |
{n,} | n是一个非负整数。至少匹配n次 |
{n,m} | m和n均为非负整数,其中n<=m。最少匹配n次且最多匹配m次。请注意在逗号和两个数之间不能有空格 |
. | 匹配除“\r\n”之外的任何单个字符。 |
x|y | 匹配x或y。例如,“z |
[xyz] | 字符集合。匹配所包含的任意一个字符。例如,“[abc]”可以匹配“plain”中的“a”。 |
[^xyz] | 负值字符集合。匹配未包含的任意字符。例如,“[^abc]”可以匹配“plain”中的“plin”。 |
[a-z] | 字符范围。匹配指定范围内的任意字符。 |
[^a-z] | 负值字符范围。匹配任何不在指定范围内的任意字符。 |
数据库中可以在where子句中的regexp字句中使用正则表达式来进行满足条件匹配。
WHERE <字段> REGEXP <’正则表达式’>