C++语言基础篇(二)

12、野(wild)指针与悬空指针(dangling)指针有什么区别?如何避免?

野指针(wild pointer): 就是没有被初始化过的指针。⽤ gcc -Wall 编译, 会出现 useduninitialized 警告。

悬空指针:是指针最初指向的内存已经被释放了的⼀种指针。
⽆论是野指针还是悬空指针,都是指向⽆效内存区域(这⾥的⽆效指的是"不安全不可控")的指针。 访问"不安全可控"(invalid)的内存区域将导致"Undefined Behavior"。
如何避免使⽤野指针? 在平时的编码中,养成在定义指针后且在使⽤之前完成初始化的习惯或者使⽤智能指针。

13、说一下const修饰指针如何区分

下⾯都是合法的声明,但是含义⼤不同:

const int * p1; //指向整形常量的指针,它指向的值不能修改
int * const p2; //指向整形的常量指针 ,它不能在指向别的变量,但指向(变量)的值可以修改。
const int *const p3; //指向整形常量 的 常量指针 。它既不能再指向别的常量,指向的值也不能修改。

理解这些声明的技巧在于,查看关键字const右边来确定什么被声明为常量 ,如果该关键字的右边是类型,则值是常量;如果关键字的右边是指针变量,则指针本身是常量。

14、简单说⼀下函数指针

从定义和⽤途两⽅⾯来说⼀下⾃⼰的理解:
⾸先是定义:函数指针是指向函数的指针变量。函数指针本身⾸先是⼀个指针变量,该指针变量指向⼀个具体的函数。这正如⽤指针变量可指向整型变量、字符型、数组⼀样,这⾥是指向函数。
在编译时,每⼀个函数都有⼀个⼊⼝地址,该⼊⼝地址就是函数指针所指向的地址。有了指向函数的指针变量后,可⽤该指针变量调⽤函数,就如同⽤指针变量可引⽤其他类型变量⼀样,在这些概念上是⼤体⼀致的。
其次是⽤途:调⽤函数和做函数的参数,⽐如回调函数。

char * fun(char * p) {
    
    ...} // 函数fun
char**pf)(char * p); //函数指针pf
pf = fun;                     // 函数指针pf指向函数fun
pf(p);                       //通过函数指针pf调用函数fun

15、堆和栈的区别


由编译器进⾏管理,在需要时由编译器⾃动分配空间,在不需要时候⾃动回收空间,⼀般保存的是局部变量和函数参数等。
连续的内存空间,在函数调⽤的时候,⾸先⼊栈的主函数的下⼀条可执⾏指令的地址,然后是函数的各个参数。

⼤多数编译器中,参数是从右向左⼊栈(原因在于采⽤这种顺序,是为了让程序员在使⽤C/C++的“函数参数⻓度可变”这个特性时更⽅便。如果是从左向右压栈,第⼀个参数(即描述可变参数表各变量类型的那个参数)将被放在栈底,由于可变参的函数第⼀步就需要解析可变。

参数表的各参数类型,即第⼀步就需要得到上述参数,因此,将它放在栈底是很不⽅便的。)
本次函数调⽤结束时,局部变量先出栈,然后是参数,最后是栈顶指针最开始存放的地址,程
序由该点继续运⾏,不会产⽣碎⽚。

栈是⾼地址向低地址扩展,栈低⾼地址,空间较⼩。

由程序员管理,需要⼿动 new malloc delete free 进⾏分配和回收,如果不进⾏回收的话,会造成内存泄漏的问题。

不连续的空间,实际上系统中有⼀个空闲链表,当有程序申请的时候,系统遍历空闲链表找到第⼀个⼤于等于申请⼤⼩的空间分配给程序,⼀般在分配程序的时候,也会在空间头部写⼊内存⼤⼩,⽅便 delete 回收空间⼤⼩。当然如果有剩余的,也会将剩余的插⼊到空闲链表中,这也是产⽣内存碎⽚的原因。

堆是低地址向⾼地址扩展,空间交⼤,较为灵活。

16、函数传递参数的几种方式

值传递 : 形参是实参的拷⻉,函数内部对形参的操作并不会影响到外部的实参。
指针传递: 也是值传递的⼀种⽅式,形参是指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进⾏操作。
引⽤传递: 实际上就是把引⽤对象的地址放在了开辟的栈空间中,函数内部对形参的任何操作以直接映射到外部的实参上⾯。

17 new / delete/,malloc / free 区别

都可以⽤来在堆上分配和回收空间。 new /delete 是操作符, malloc和free是库函数。

执⾏ new 实际上执⾏两个过程: 1.分配未初始化的内存空间(malloc); 2.使⽤对象的构造函数对空间进⾏初始化;返回空间的⾸地址。如果在第⼀步分配空间中出现问题,则抛出std::bad_alloc 异常,或被某个设定的异常处理函数捕获处理;如果在第⼆步构造对象时出现异常,则⾃动调⽤ delete 释放内存。
执⾏ delete 实际上也有两个过程: 1. 使⽤析构函数对对象进⾏析构; 2.回收内存空间(free)。

以上也可以看出 new 和 malloc 的区别, new 得到的是经过初始化的空间,⽽ malloc 得到的是未初始化的空间。所以 new 是 new ⼀个类型,⽽ malloc 则是malloc ⼀个字节⻓度的空间。 delete 和 free 同理, delete 不仅释放空间还析构对象, delete ⼀个类型, free ⼀个字节⻓度的空间。

为什么有了 malloc/ free 还需要 new/ delete? 因为对于⾮内部数据类型⽽⾔,光⽤ malloc/ free ⽆法满⾜动态对象的要求。 对象在创建的同时需要⾃动执⾏构造函数,对象在消亡以前要⾃动执⾏析构函数 。由于 mallo/ free 是库函数⽽不是运算符,不在编译器控制权限之内,不能够把执⾏的构造函数和析构函数的任务强加于 malloc/ free , 所以有了 new/ delete 操作符。

18、volatile和extern 关键字

volatile 三个特性
易变性:在汇编层⾯反映出来,就是两条语句,下⼀条语句不会直接使⽤上⼀条语句对应的volatile 变量的寄存器内容,⽽是重新从内存中读取。

不可优化性: volatile 告诉编译器,不要对我这个变量进⾏各种激进的优化,甚⾄将变量直接消除,保证程序员写在代码中的指令,⼀定会被执⾏。
顺序性: 能够保证 volatile 变量之间的顺序性,编译器不会进⾏乱序优化。
extern

在 C 语⾔中,修饰符 extern ⽤在变量或者函数的声明前,⽤来说明 “此变量/函数是在别处定
义的,要在此处引⽤”。

注意 extern 声明的位置对其作⽤域也有关系,如果是在 main 函数中进⾏声明的,则只能在main 函数中调⽤,在其它函数中不能调⽤。其实要调⽤其它⽂件中的函数和变量,只需把该⽂件⽤ #include 包含进来即可,为啥要⽤ extern?因为⽤ extern 会加速程序的编译过程,这样能节省时间。

在 C++ 中 extern 还有另外⼀种作⽤,⽤于指示 C 或者 C++函数的调⽤规范。⽐如在 C++ 中调⽤ C 库函数,就需要在 C++ 程序中⽤ extern “C” 声明要引⽤的函数。这是给链接器⽤的,告诉链接器在链接的时候⽤C 函数规范来链接。主要原因是 C++ 和 C 程序编译完成后在⽬标代码中命名规则不同,⽤此来解决名字匹配的问题。

19、define和const区别(编译阶段,安全性,内存占用等)

对于 define 来说, 宏定义实际上是在预编译阶段进⾏处理,没有类型,也就没有类型检查,仅仅做的是遇到宏定义进⾏字符串的展开,遇到多少次就展开多少次,⽽且这个简单的展开过程中,很容易出现边界效应,达不到预期的效果。因为 define 宏定义仅仅是展开,因此运⾏时系统并不为宏定义分配内存,但是从汇编 的⻆度来讲, define 却以⽴即数的⽅式保留了多份数据的拷⻉。

对于 const 来说, const 是在编译期间进⾏处理的, const 有类型,也有类型检查,程序运⾏时系统会为 const 常量分配内存,⽽且从汇编的⻆度讲, const 常量在出现的地⽅保留的是真正数据的内存地址,只保留了⼀份数据的拷⻉,省去了不必要的内存空间。⽽且,有时编译器不会为普通的 const 常量分配内存,⽽是直接将 const 常量添加到符号表中,省去了读取和写⼊内存的操作,效率更⾼。

20、计算下面几个类的大小

class A{
    
    }; sizeof(A) = 1;//孔磊在实例化时得到一个独一无二的地址,所以为1.
class A{
    
    virtual Fun(){
    
    }}; sizeof(A) = 4(32bit)/8(64bit)//当c++中有虚函数的时候,会有一个指向虚函数表的指针(vptr)
class A{
    
    static int a;}; sizeof(A)=1;
class A{
    
    int a;}; sizeof(A) = 4;
class A{
    
    static int a; int b;}; sizeof(A) = 4;

21、面向对象的三大特性,并举例说明

C++面向对象的三大特性时:封装、继承、多态。
所谓封装
就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让信任的类或者对象操作,对不可信的进行隐藏。一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或者某些数据是可以私有的,不可以被外界访问,通过这种方式,对象的内部数据提供了不同级别的保护,以防止程序中无关的部分信息以外的改变或错误的使用了对象的私有部分。

所谓继承
是指可以让某个类型的对象获得另一个类型的对象的属性的方法。它支持按级分类的概念。继承是指这样的一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行拓展。通过继承创建的新类称为“子类”或者“派生类”,被继承的类称为“基类”,“父类”或“超类”。继承的过程,就是从一般到特殊的过程。要实现继承,可以通过“继承”和“组合”来实现。

继承概念的实现⽅式有两类:

实现继承:实现继承是指直接使⽤基类的属性和⽅法⽽⽆需额外编码的能⼒。

接⼝继承:接⼝继承是指仅使⽤属性和⽅法的名称、但是⼦类必需提供实现的能⼒。

所谓多态
就是向不同的对象发送同⼀个消息,不同对象在接收时会产⽣不同的⾏为(即⽅法)。即⼀个接⼝,可以实现多种⽅法。

多态与⾮多态的实质区别就是函数地址是早绑定还是晚绑定的。如果函数的调⽤,在编译器编译期间就可以确定函数的调⽤地址,并产⽣代码,则是静态的,即地址早绑定。⽽如果函数调⽤的地址不能在编译器期间确定,需要在运⾏时才确定,这就属于晚绑定。

猜你喜欢

转载自blog.csdn.net/qq_43679351/article/details/124931688