零碎知识点
1. new、delete、malloc、free
:smile:
new 和 delete对应,是C++语言的标准库函数
malloc和free对应,是C++的运算符
它们都可用于申请动态内存和释放内存,区别在对非内部数据类型的对象而言,malloc和free无法满足动态对象的要求(因为对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数)
2. delete和delete[] 的区别
delete只会调用一次析构函数
但是delete[]会调用每一个成员的析构函数
3. C++有哪些性质
封装、继承和多态
封装:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。(private,protected,public)
继承: 继承允许我们依据另一个类来定义一个类。有三种继承方式,决定了派生类对于基类的访问权限。:smile: 接口继承,就是派生类只继承函数的接口,也就是声明;而实现继承,就是派生类同时继承函数的接口和实现。
多态:函数的多种不同实现方式,C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。详细见(4)
4. 多态,虚函数,纯虚函数
虚函数
virtual与基类虚函数有相同的参数个数参数的类型与基类相同
返回值与基类相同,或者都返回指针或引用,并且派生类虚函数所返回的指针或引用的基类型是基类中被替换的虚函数所返回的指针或引用的基类型的子类型。
纯虚函数
是一种特殊的虚函数,在基类中声明但是在基类中没有定义,要求任何派生类定义具体的实现方法。
1 | class <类名> |
抽象类
包含纯虚函数的类称为抽象类。
由于抽象类包含了没有定义的纯虚函数,所以不能定义抽象类的对象。抽象类是一种特殊的类,它是为了抽象和设计的目的而建立的,它处于继承层次结构的较上层。抽象类是不能定义对象的,在实际中为了强调一个类是抽象类,可将该类的构造函数说明为保护的访问控制权限。
多态性
同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,这就是多态性。
编译时的多态性:编译时的多态性是通过重载来实现的。(静态多态)
运行时的多态性:运行时的多态性就是指直到系统运行时,才根据实际情况决定实现何种操作,例如虚函数。(动态多态)
多态的作用:
1.隐藏实现细节,使得代码能够模块化,并且能够扩展代码模块,实现代码重用
2.接口重用:为了类在继承和派生的时候,保证使用家族中任一类的实例的某一属性时的正确调用
5. 引用
申明引用的时候记得一定要初始化。
引用不是一种数据类型,本身不占存储单元
引用作为函数参数的特点:
- 传递引用给函数和指针是一样的
- 使用引用传递函数的参数,在内存中没有产生实际参数的副本,传递的效率较高
- 使用指针作为函数的参数,需要分配存储单元,并且调用时必须使用变量的地址作为实参;引用相比指针更清晰,可读性好。
常引用
常引用声明方式:const 类型标识符 &引用名=目标变量名;
可以保护传递给函数的数据不在函数中被改变
引用作为函数返回值:
- 优点:在内存中不产生被返回值的副本
- 需要遵守的规则:
- 不能返回局部变量
- 不能返回函数内部new分配的内存引用
- 可以返回类成员的引用,最好是const。
- 赋值操作符(赋值操作符的返回值必须是一个左值)、流操作符重载返回值申明为“引用”
- 在另外的一些操作符中,不能返回引用:+-*/ 四则运算符
引用也可以产生多态效果,这意味着,一个基类的引用可以指向它的派生类实例。
除了
>><<=之外的情况,都推荐使用引用。
6. 联合
在“联合”中,各成员共享一段内存空间,一个联合变量的长度等于各成员中最长的长度。在同一时刻,联合中只存放了一个成员,所有成员公用一个地址空间。
例如:
1 |
|
7. 聚合和组合 Aggregation & Composition
当一个类的对象拥有另一个类的对象时,就会发生类聚合。聚合表示has-a的关系,是一种相对松散的关系,聚合类不需要对被聚合类负责,用空的菱形表示聚合关系,从实现的角度讲,聚合可以表示为:
1 | class A |
组合表示contains-a的关系,关联性强于聚合;组合类与被组合类有相同的生命周期,组合类要对被组合类负责,采用实心的菱形表示组合关系:实现的形式是:
1 | class A |
8. 重载(overload)
是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。
9. C++是类型安全的吗?
不是,两个不同类型的指针可以强制转换。
C# 是类型安全的。
10. main函数执行之前会执行什么代码
全局对象的构造函数
11. 当一个类A中没有任何成员变量与成员函数,这时sizeof(A)的值是多少?
空类大小等于1,是因为编译器为了区分不同的类,在类中加的一个char型。
12. 比较C++中的4种类型转换方式?
static_cast
static_cast能够完成指向相关类的指针上的转换。upcast和downcast都能够支持,但不同的是,并不会有运行时的检查来确保转换到目标类型上的指针所指向的对象有效且完整。因此,这就完全依赖程序员来确保转换的安全性。但反过来说,这也不会带来额外检查的开销。
dynamic_cast
dynamic_cast 只能用在指向类的指针或者引用上,允许upcast(从派生类向基类的转换)。在一颗类继承树上转换时,将利用 RTTI(Run-Time Type Identification) 在运行时检查,我们一般用于 downcast
1
2
3
4
5
6
7class A {};
class B : public A {};
A* a = new B();
B* b;
b = dynamic_cast<B*>(a);reinterpret_cast
reinterpret_cast能够完成任意指针类型向任意指针类型的转换,即使它们毫无关联。该转换的操作结果是出现一份完全相同的二进制复制品,既不会有指向内容的检查,也不会有指针本身类型的检查。
:scream: 有点危险
const_cast
const_cast可以用来设置或者移除指针所指向对象的const。
:open_mouth: 比较危险
13. 内存分配方式以及区别
1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集。
3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由程序员决定,使用非常灵活,但问题也最多。
14. const相比#define有什么优点
- const常量有数据类型,而宏常量没有数据类型,编译器可以对前者进行安全检查,但是对define只能进行字符替换,并且在字符替换时会发生其他意料之外的错误
- 有些集成化的调试工具可以对const常量进行调试,但是对宏常量不能进行调试
15. 简述数组和指针的区别
- 数组要么在静态存储区被创建(全局数组),要么在栈上被创建。指针可以随时指向任意类型的内存块。
- 用运算符sizeof 可以计算出数组的容量(字节数)。sizeof(p),p 为指针得到的是一个指针变量的字节数,而不是p所指的内存容量。C++/C 语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。在32位平台下,无论指针的类型是什么,sizeof(指针名)都是4,在64位平台下,无论指针的类型是什么,sizeof(指针名)都是8。
- 对于赋值对于第二种语句,因为指针没有分配自己的存储空间,相当于只是一个数据块,没有办法直接访问,这样的赋值只能读不能写。需要进行内存分配才可以。
1
2
3
4
5char a[3] = "abc";
strcpy(a,"end");//it's ok
char *a = "abc";
strcpy(a,"end"); //it's wrong1
2
3
4char *a = new char[4];
strcpy(a, "end");
printf("%s", a);
delete []a;
16. 类成员函数的重载、覆盖和隐藏的区别
重载
(1) 在同一个类中,具有相同的范围
(2) 函数名相同
(3) 参数不同
(4) virtual关键字可有可无
覆盖,是指派生类函数覆盖了基类函数
(1) 不同的范围(分别在派生类和基类中)(2) 函数名相同
(3) 参数乡同
(4) 基类函数必然有virtual关键字
隐藏是指派生类函数屏蔽了与其同名的基类函数
(1) 如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)(2) 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)
17. 一个语句求出两个数中的较大值
((a+b)+abs(a-b))/2
18. 如何判断一段程序是由C 编译程序还是由C++编译程序编译的?
1 |
|
19. main主函数执行完毕后,是否可能会再执行一段代码
可以用_onexit注册一个函数
1 | void main(void) |
20. 指针找错题
1
2
3
4
5
6void test1()
{
char string[10];
char* str1 = "0123456789";
strcpy( string, str1 );
}数组越界,字符串str1需要11个字节(末尾的’/0’)
1
2
3
4
5
6
7
8
9
10void test2()
{
char string[10], str1[10];
int i;
for(i=0; i<10; i++)
{
str1= 'a';
}
strcpy( string, str1 );
}对于str1的赋值没有在末尾加入’/0’结束符,在strcpy的时候会非常危险,所复制的字节数具有不确定性。
1
2
3
4
5
6
7
8void test3(char* str1)
{
char string[10];
if( strlen( str1 ) <= 10 )
{
strcpy( string, str1 );
}
}代码中的小于等于应修改为小于,因为strlen并不会统计字符串结尾的’/0’
1
2
3
4
5
6
7
8
9
10
11void GetMemory( char *p )
{
p = (char *) malloc( 100 );
}
void Test( void )
{
char *str = NULL;
GetMemory( str );
strcpy( str, "hello world" );
printf( str );
}传入GetMemory函数的形参为字符串指针,在函数内部修改形参并不能真正的改变传入形参的值,str仍未NULL
5.
1 | void GetMemory( char **p, int num ) |
未判断内存是否申请成功,而且未对malloc的内存进行释放\
1
2
3
4
5
6
7
8
9
10
11char *GetMemory( void )
{
char p[] = "hello world";
return p;
}
void Test( void )
{
char *str = NULL;
str = GetMemory();
printf( str );
}p[]数组为函数内的局部自动变量,在函数返回后,内存已经被释放。这是许多程序员常犯的错误,其根源在于不理解变量的生存期。
对内存操作的考查主要集中在:- 指针的理解
- 变量的生存期和作用范围
- 良好的动态内存申请和释放习惯
21. 编写一个标准strcpy函数
1 | char * strcpy( char *strDest, const char *strSrc ) |
- 将源字符串加const,表明其为输入参数
- 对源地址和目的地址加非空断言
- 为了实现链式操作,将地址返回
同理,标准的strlen函数:
1 | int strlen( const char *str ): |
22. 请问交换机和路由器各自的实现原理是什么?分别在哪个层次上面实现的?
交换机:数据链路层;
路由器:网络层。