C++ 基础知识 面试题(一)

1.变量的声明与定义

声明:int x; //告诉编译器这个变量的类型和名称

定义:int x = 0; //告诉编译器这个变量的类型和名称,为该变量分配内存空间,并初始化该变量

主要区别在于是否为变量分配内存空间

2.extern关键字

用法一:在声明时修饰函数或变量,表明该函数或变量定义其他文件中,告诉编译器去其他文件中寻找函数或变量的定义。

extern int a;

extern void func();

当一个变量要在多个文件中使用时,只需在一个文件中定义该变量,然后再其他文件中使用extern声明这个变量。

用法二:extern "C" 

C++会对函数名进行名称修饰(Name Mangling),以便支持函数重载和命名空间等特性。

也就是说,编译器会在编译函数的过程中将函数的参数类型也加到函数名后面。

但是C语言不支持函数重载,编译时不会加上函数的参数类型,一般只有函数名。

所以当我们要在C++中调用C语言的代码时,不能直接按照C++的规则进行名称修饰。

所以可以在前面加上extern "C" ,表明这一段是C语言代码,按照C语言的规则编译。

Example:

C++中直接使用C代码:

// myfunc.cpp
#include <stdio.h>

extern "C" {
    void myFunc() {
        printf("Hello from myFunc!\n");
    }
}

C++中调用C语言头文件:

// main.cpp
#include <iostream>

extern "C" {
    #include "mylib.h"
}

int main() {
    myFunc();
    std::cout << "myVar = " << myVar << std::endl;
    return 0;
}

3.内存类型

栈内存(Stack Memory):栈内存是由编译器自动分配和释放的,用于存储局部变量和函数参数等数据。栈内存的大小是固定的,通常比堆内存小,且分配和释放速度较快。当函数执行完毕时,栈内存中的数据会自动被销毁。

堆内存(Heap Memory):堆内存是由程序员手动分配和释放的,用于存储动态分配的数据。堆内存的大小是不固定的,可以根据需要进行动态调整,但分配和释放速度较慢。需要注意的是,堆内存中的数据需要手动释放,否则会导致内存泄漏。

全局内存(Global Memory):全局内存是由编译器自动分配和释放的,用于存储全局变量和静态变量等数据。全局内存的大小是固定的,程序运行期间始终存在,直到程序结束才会被释放。

常量内存(Constant Memory):常量内存是用于存储常量数据的内存区域,通常包括字符串常量、枚举常量和const修饰的变量等。常量内存的大小是固定的,程序运行期间始终存在,直到程序结束才会被释放。

关于内存类型,还会涉及到自由存储区

自由存储区是一个抽象的概念,指的是C++中通过new()和delete()动态分配和释放的内存区域。

C++编译器默认使用堆来实现自由存储区,但是自由存储区不等价于堆。

堆是操作系统维护的一块动态分配内存,通过malloc()和free()来分配和释放。

4.C++程序编译的过程:

  • 预处理:C++编译器会先对源代码进行预处理,将所有的#include指令替换为对应的头文件内容,宏定义替换为对应的文本等。
  • 编译:编译器将预处理后的源代码翻译成汇编代码,这个过程包括词法分析、语法分析、语义分析等步骤。
  • 汇编:汇编器将汇编代码转换为机器码,这个过程包括生成目标文件、符号解析、重定位等步骤。
  • 链接:连接器将目标文件和库文件链接在一起,生成可执行文件。这个过程包括符号解析、重定位等步骤。

编译过程中生成的文件流程图:

5. malloc和new的区别

语法:C语言中使用malloc函数动态分配内存,而C++中使用new运算符动态分配内存

参数:malloc函数只接受一个参数,即要分配的内存大小,而new运算符可以接受一个参数(分配单个对象内存)或多个参数(分配数组内存)

        eg: Person* arr = new Person[size] { {"Tom", 20}, {"Jerry", 18}, {"Alice", 22} };

返回值:malloc函数返回void*类型的指针,需要进行强制类型转换才能使用;new运算符返回分配的对象类型的指针,不需要进行强制类型转换

内存初始化:malloc函数分配的内存不会被初始化,而new运算符分配的内存会被初始化为默认值(0或空指针)或用户指定的值

异常处理:malloc函数在分配内存失败时返回空指针,new运算符在分配内存失败时会抛出std::bad_alloc异常

6.计算机内部存储负数的方式

最常用的方式是二进制补码表示法。

在二进制补码表示法中,最高位(即最左边的位)为符号位,0表示正数,1表示负数。

例如,十进制数-42的8位二进制表示为11010110。

其计算方法为,先用原码表示-42,即将42的二进制(00101010)表示取反,得到11010101,然后将结果加1,得到11010110,这就是-42的二进制补码表示。

7.计算机内部存储浮点数的方式

计算机内部存储浮点数的方式是采用IEEE 754标准。

在IEEE 754标准中,一个浮点数由三个部分组成:符号位、指数和尾数。

IEEE 754标准定义了两种浮点数表示方式:单精度浮点数(4个字节)和双精度浮点数(8个字节)。

在单精度浮点数中,第1位为符号位,接下来的8位为指数部分,最后的23位为尾数部分。

在双精度浮点数中,第1位为符号位,接下来的11位为指数部分,最后的52位为尾数部分。

对于指数部分,IEEE 754标准采用了偏移值的表示方法。具体来说,指数部分的实际值等于指数字段的值减去一个偏移值。

在单精度浮点数中,偏移值为127,在双精度浮点数中,偏移值为1023。

对于尾数部分,IEEE 754标准采用了规格化和非规格化的表示方法。

规格化的尾数部分表示的是一个范围在1到2之间的实数,非规格化的尾数部分表示的是一个范围在0到1之间的实数。

举例:单精度浮点数

例如,我们要存储十进制数-3.25,其二进制表示为-11.01(整数部分除2取余再翻转,小数部分乘2取整)

根据IEEE 754标准,我们需要将其转换为科学计数法的形式,即-1.101 x 2^1。

其中,符号位为1,指数部分为1+127=128(偏移值为127),尾数部分为10100000000000000000000。

将这三部分按照规定的格式组合起来,就可以得到单精度浮点数的二进制表示:1 10000000 10100000000000000000000。其中,第1位为符号位,接下来的8位为指数部分,最后的23位为尾数部分。

8.内存泄漏

内存泄漏是指程序在运行过程中申请的内存空间没有被及时释放,导致系统中的可用内存不断减少,最终可能导致系统崩溃或性能下降的问题。

以下是一些常用的方法:

  • 使用内存分配和释放函数时要小心,确保申请的内存空间在不需要时及时释放。
  • 避免使用全局变量,因为全局变量的生命周期很长,容易导致内存泄漏。
  • 使用智能指针和垃圾回收机制来管理内存,这样可以自动释放不再使用的内存空间。
  • 使用静态分析工具来检测潜在的内存泄漏问题。
  • 对于C++等语言,可以使用RAII技术,即在对象的构造函数中申请内存,在析构函数中释放内存,从而确保内存的正确释放。
  • 在对指针赋值前,要考虑是否释放指针原先指向的内存。

9.volatil关键字

用于告诉编译器一个变量可能会被意外地修改。

它的作用是防止编译器对变量的优化,保证程序的正确性。

当一个变量被声明为volatile时,编译器将不会对该变量进行优化,例如,不会将该变量的值缓存到CPU寄存器中。这是因为该变量的值可能会被其他线程或硬件设备修改,如果编译器对该变量进行了优化,就可能导致程序出错。

10. final关键字

用于修饰类、成员函数和虚函数。它的作用是禁止继承、重载和覆盖。

当一个类被声明为final时,它不能被其他类继承。

当一个成员函数被声明为final时,它不能被子类重载或覆盖。

当一个虚函数被声明为final时,它不能被子类覆盖。

当一个变量被声明为final时,它只能赋一次值。

当一个引用变量被声明为final时,它只能赋一次值,并且它只能永远指向该对象,无法指向其他对象。虽然final的引用指向对象A后,不能再重新指向对象B,但是对象A内部的数据可以被修改。

final修饰的实例变量一般添加 static修饰。static final联合修饰的变量称为常量。
 

11.多态

静态多态和动态多态都是面向对象编程中的概念,它们都涉及到多态性的实现。

静态多态是指在编译时就能够确定调用的函数,也称为编译时多态。它是通过函数重载和运算符重载实现的,编译器会根据参数的类型和数量来决定调用哪个函数或运算符。静态多态的优点是效率高,因为编译器能够在编译时确定函数调用,不需要在运行时进行类型检查。

动态多态是指在运行时才能够确定调用的函数,也称为运行时多态。

它允许同一个函数名在不同的对象中具有不同的行为。多态的实现原理是通过虚函数来实现的。

在C++中,通过在函数声明前面添加关键字virtual来将函数声明为虚函数。当一个类中的函数被声明为虚函数时,它可以被子类重写。

使用虚函数可以使程序更加灵活,可以根据不同的对象类型调用不同的函数实现。

编译器会在基类中为虚函数生成一个虚函数表,派生类会继承这个虚函数表并重写其中的虚函数。虚函数的实现依赖于虚函数表,每个对象都有一个指向虚函数表的指针,通过该指针可以在运行时确定应该调用哪个函数。

通过基类指针或引用调用虚函数(普通变量不会调用虚函数)。在运行时,虚函数会根据对象的实际类型来调用相应的函数,从而实现多态。

#include <iostream>
using namespace std;

class Animal {
public:
    virtual void speak() {
        cout << "This is an animal speaking." << endl;
    }
};

class Cat : public Animal {
public:
    void speak() {
        cout << "Meow!" << endl;
    }
};

class Dog : public Animal {
public:
    void speak() {
        cout << "Woof!" << endl;
    }
};

int main() {
    Animal* animal1 = new Cat();
    Animal* animal2 = new Dog();
    animal1->speak();// it will print "Meow!"
    animal2->speak();// it will print "Woof!"
    return 0;
}

12.引用和指针的区别

  • 指针可以为空,而引用不能。指针可以指向空值,即nullptr,而引用必须引用一个有效的对象。
  • 指针可以进行算术运算,而引用不能。指针可以进行加、减等运算,而引用是引用的变量值加一。
  • 指针是一个实体,需要分配内存空间。引用只是变量的别名,不需要分配内存空间。
  • 引用在定义的时候必须进行初始化,并且不能够改变。指针在定义的时候不一定要初始化,并且可以被重新赋值。
  • 有多级指针,但是没有多级引用,只能有一级引用。
  • sizeof 引用得到的是所指向的变量(对象)的大小,而sizeof 指针得到的是指针本身的大小。
  • 引用在内部实现上是一个指针,但是它在使用时,像一个变量一样使用。这使得使用引用更加方便和直观。
  • 作为参数时也不同,传指针的实质是传值,传递的值是指针的地址;传引用的实质是传地址,传递的是变量的地址。

13.函数重写和重载的区别

函数重写(Overriding):在继承关系中,子类可以对父类的某个方法进行重写,以满足自己的需求。子类重写父类的方法时,方法名、参数列表和返回值类型必须与父类的方法相同,但是方法体可以不同。在运行时,如果调用的是子类对象的该方法,那么就会执行子类中的方法体,而不是父类中的方法体。

在C++中,如果想要在派生类中重写基类的函数,通常需要在基类的函数声明中添加virtual关键字,这样才能实现运行时多态。如果基类函数没有使用virtual关键字,那么在派生类中重写该函数只会隐藏基类的同名函数,而不会实现多态

函数重载(Overloading):在同一个类中,可以定义多个同名的方法,但是它们的参数列表必须不同。参数列表可以包括参数的类型、个数、顺序等。在调用该方法时,编译器会根据传入的参数类型和数量来选择合适的方法进行调用。

总的来说,函数重写是子类对父类某个方法的重新实现,而函数重载是同一个类中对同名方法的多次定义。

14.面向对象编程(Object-Oriented Programming,简称 OOP)具有以下三大特征:

1) 封装(Encapsulation):
   - 封装是将数据和操作数据的方法(即类的成员变量和方法)组合在一起,形成一个称为类的实体。
   - 封装隐藏了数据的具体实现细节,只提供公共接口供外部访问和操作数据,也增加了对数据和方法的访问控制(private, protected, public)
   - 通过封装,可以实现数据的安全性和灵活性,使得代码更加模块化和可维护。

2) 继承(Inheritance):
   - 继承是指一个类(称为子类或派生类)可以从另一个类(称为父类或基类)继承属性和方法。
   - 继承使得子类可以重用父类的代码,避免了重复编写相同的代码。
   - 子类可以扩展或修改从父类继承的属性和方法,也可以添加自己特有的属性和方法。
   - 继承提供了代码的层次化组织,使得代码更加可扩展和可维护。

3) 多态(Polymorphism):
   - 多态是指同一个方法名可以在不同的对象上具有不同的实现方式。
   - 多态通过方法的重写和方法的重载实现。
   - 方法的重写(Override)指子类重写父类的方法,使得子类对象调用该方法时执行子类的实现逻辑。
   - 方法的重载(Overload)指在同一个类中定义多个方法,它们具有相同的方法名但不同的参数列表。
   - 多态提高了代码的灵活性和可扩展性,使得同一段代码可以适用于不同类型的对象。

15.空类

在 C++ 中,空类(Empty Class)指的是没有显式声明任何成员变量或成员函数的类。它是一种最简单的类定义形式,没有任何数据成员或成员函数的定义。空类可以用于一些特定的编程场景,例如作为基类或占位符

下面是一个示例,展示了一个空类的定义:

```cpp
class EmptyClass {
    // Empty class with no member variables or member functions
};
```

在这个示例中,`EmptyClass` 是一个空类,没有任何成员变量或成员函数的定义。

空类的主要用途之一是作为基类,用于派生其他类。通过继承空类,子类可以继承空类的特性,并添加自己的成员变量和成员函数。这种用法在实现多态和组织代码结构时很常见。

另外,空类也可以用作占位符,作为一种标记或占位的作用。例如,在某些设计模式中,可以使用空类作为标记类来表示某个特定的概念或行为。

需要注意的是,即使是空类,编译器在没有显式定义成员函数时,仍会为其生一些默认的成员函数。这些默认生成的成员函数包括:

1. 默认构造函数(Default Constructor):
   - 默认构造函数是没有参数的构造函数。
   - 如果你没有显式定义任何构造函数,编译器会自动生成一个默认构造函数。
   - 默认构造函数用于创建类的对象,并初始化其成员变量。

2. 默认析构函数(Default Destructor):
   - 默认析构函数没有参数。
   - 如果你没有显式定义任何析构函数,编译器会自动生成一个默认析构函数。
   - 默认析构函数用于在对象被销毁时清理资源。

3. 默认拷贝构造函数(Default Copy Constructor):
   - 默认拷贝构造函数用于创建一个新对象,并将其初始化为另一个同类型对象的副本。
   - 如果你没有显式定义任何拷贝构造函数,编译器会自动生成一个默认拷贝构造函数。
   - 默认拷贝构造函数执行逐个成员变量的复制。

4. 默认赋值运算符(Default Assignment Operator):
   - 默认赋值运算符用于将一个对象的值赋给另一个同类型的对象。
   - 如果你没有显式定义任何赋值运算符,编译器会自动生成一个默认赋值运算符。
   - 默认赋值运算符执行逐个成员变量的赋值。

需要注意的是,只有当实际使用这些函数的时候,编译器才会去定义它们。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值