C++面试题
Posted 多一些不为什么的坚持
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++面试题相关的知识,希望对你有一定的参考价值。
1、当使用C++编写代码时,有一个常见的问题是如何在子类中调用父类的构造函数。下面是一个相关的C++面试题:
题目: 假设有一个基类Animal,其中包含一个带参数的构造函数和一个公共成员函数display()。请编写一个派生类Dog,继承自Animal,并实现自己的构造函数和display()函数。
要求:
- Dog类的构造函数应接受两个参数:name(狗的名字)和age(狗的年龄)。构造函数应该在初始化Dog对象时调用Animal类的构造函数,并将name参数传递给Animal类的构造函数来初始化基类的成员变量。
- Dog类的display()函数应输出狗的名字和年龄。
示例代码:
#include <iostream> #include <memory> #include <iostream> class Animal protected: std::string name; public: Animal(const std::string& n) : name(n) std::cout<<"1111111"<<std::endl; void display() std::cout << "Name: " << name << std::endl; ; class Dog : public Animal private: int age; public: Dog(const std::string& n, int a) : Animal(n), age(a) std::cout<<"222222"<<std::endl; void display() Animal::display(); std::cout << "Age: " << age << std::endl; ; class Cat : public Animal private: int age; public: Cat(const std::string& n, int a) : Animal(n), age(a) std::cout<<"3333333333"<<std::endl; void display() Animal::display(); std::cout << "Age: " << age << std::endl; ; int main() Dog dog("Buddy", 3); Cat cat("axis", 4); dog.display(); cat.display(); return 0;
输出:
1111111
222222
1111111
3333333333
Name: Buddy
Age: 3
Name: axis
Age: 4
两个派生类分别继承了基类,但是对于基类互相不影响,先调用基类的构造函数。
于每个派生类对象而言,它们都有自己独立的基类部分。当多个派生类继承同一个基类时,每个派生类对象中的基类部分是相互独立的。
当一个派生类对象修改了基类的成员变量,只会影响到该派生类对象自身的基类成员变量,不会影响其他派生类对象的基类成员变量。
在您提供的代码中,Dog
类和 Cat
类都继承自基类 Animal
,但它们各自拥有独立的基类部分。当您创建 Dog
对象和 Cat
对象时,它们各自的基类部分是分开的,修改其中一个派生类对象的基类成员变量不会影响另一个派生类对象的基类成员变量。
换句话说,Dog
类和 Cat
类的基类成员变量 name
是独立的,并不会相互影响。无论是在 Dog
类的构造函数还是 Cat
类的构造函数中修改基类 Animal
的 name
成员变量,只会影响到各自的对象,不会影响其他对象。
所以,当一个派生类对象修改了基类的成员变量,只会影响到该派生类对象自身的基类成员变量,不会影响其他派生类对象的基类成员变量。
2、这是一道经典的C++面试题:
#include <iostream> using namespace std; class Base public: Base() cout << "Constructor of Base class" << endl; virtual ~Base() cout << "Destructor of Base class" << endl; virtual void foo() cout << "Base::foo()" << endl; ; class Derived : public Base public: Derived() cout << "Constructor of Derived class" << endl; ~Derived() cout << "Destructor of Derived class" << endl; void foo() override cout << "Derived::foo()" << endl; ; int main() Base* basePtr = new Derived(); basePtr->foo(); delete basePtr; return 0;
请问,上述代码的输出是什么?解释一下输出的原因。
这道题主要考察了虚函数、多态和对象的生命周期。代码中定义了一个基类 Base
和一个派生类 Derived
,并且在 main
函数中创建了一个指向派生类对象的基类指针。
输出结果如下:
Constructor of Base class Constructor of Derived class Derived::foo() Destructor of Derived class Destructor of Base class
输出的原因是:
-
首先,创建了一个基类指针
basePtr
并将其指向派生类对象。在这个过程中,会先调用基类Base
的构造函数,然后调用派生类Derived
的构造函数。 -
接下来,调用
basePtr->foo()
,由于foo()
是虚函数且被派生类重写了,因此会调用派生类Derived
的foo()
函数。 -
最后,调用
delete basePtr
,这会先调用派生类Derived
的析构函数,然后调用基类Base
的析构函数。析构函数的调用顺序与构造函数的调用顺序相反。
这道题考察了虚函数的动态绑定,当通过基类指针调用虚函数时,会根据指针指向的对象的实际类型来确定调用的函数。同时也考察了对象的生命周期,创建对象时先调用构造函数,删除对象时先调用析构函数。
3、C++中内存分配的情况
堆:用于存储动态分配的对象。堆上的内存需要手动分配和释放,一般使用new
和delete
运算符(或者在C++11及以上版本中使用std::malloc
和std::free
函数)进行操作。堆上的内存分配和释放需要程序员显式管理,如果没有正确释放堆上分配的内存,就会导致内存泄漏。
栈:栈是一块连续的内存区域,用于存储局部变量、函数参数、函数调用信息等。栈上的内存分配和释放是自动管理的,当一个函数被调用时,其局部变量和参数会被分配到栈上,当函数调用结束时,这些变量所占用的栈空间会自动释放。
全局/静态存储区:全局存储区用于存储全局变量和静态变量,它们在程序的整个生命周期内都存在。全局变量在定义时会被分配在全局存储区,静态变量(包括静态全局变量和静态局部变量)也存储在这个区域。全局/静态存储区在程序启动时分配,在程序结束时释放。
常量存储区:常量区用于存储字符串常量和其他常量数据。这些常量数据在程序运行期间是只读的,不能被修改。常量区的数据通常是在程序编译阶段就确定的,因此在运行时无法对其进行修改。
代码区:存放程序的二进制代码。
4、请解释什么是虚函数(Virtual Function)以及它在C++中的作用。
虚函数是在基类中声明的带有关键字virtual
的成员函数。它用于实现运行时多态性(Runtime Polymorphism)和动态绑定(Dynamic Binding)的概念。在C++中,通过基类的指针或引用调用虚函数时,实际调用的是派生类中相应的虚函数。
虚函数的作用是允许在基类和派生类之间建立一种多态的关系,使得通过基类指针或引用调用的函数可以根据实际指向的对象类型而调用不同的函数实现。这样可以在运行时根据对象的实际类型来确定调用哪个函数,实现了动态绑定。
使用虚函数可以实现基于继承的多态性,提高代码的灵活性和可扩展性。它允许在基类中定义通用的接口和行为,并在派生类中进行特定的实现。这样,通过基类指针或引用可以调用派生类的特定实现,实现了统一的接口进行多样化的操作。
需要注意的是,虚函数只能用于类的成员函数,并且在派生类中覆盖(Override)基类中的虚函数时需要使用virtual
关键字进行声明。同时,虚函数的调用会引入一定的性能开销,因为需要在运行时进行动态查找,所以在设计中应谨慎使用。
#include <iostream> class Animal public: virtual void sound() std::cout << "Animal makes a sound." << std::endl; ; class Cat : public Animal public: void sound() override std::cout << "Cat says Meow!" << std::endl; ; class Dog : public Animal public: void sound() override std::cout << "Dog says Woof!" << std::endl; ; int main() Animal* animalPtr; Cat cat; Dog dog; animalPtr = &cat; animalPtr->sound(); // 输出:Cat says Meow! animalPtr = &dog; animalPtr->sound(); // 输出:Dog says Woof! return 0;
在上面的代码中,我们定义了一个基类 Animal
和两个派生类 Cat
和 Dog
。基类 Animal
中声明了一个虚函数 sound()
。派生类 Cat
和 Dog
分别重写了 sound()
函数。
在 main()
函数中,我们创建了一个指向基类 Animal
的指针 animalPtr
。我们将这个指针分别指向 Cat
对象和 Dog
对象,并通过该指针调用 sound()
函数。
由于 sound()
函数在基类中被声明为虚函数,并且在派生类中进行了重写,因此通过 animalPtr
指针调用的 sound()
函数会根据实际指向的对象类型而调用不同的实现。
当 animalPtr
指向 Cat
对象时,调用 sound()
函数会输出 "Cat says Meow!"。当 animalPtr
指向 Dog
对象时,调用 sound()
函数会输出 "Dog says Woof!"。这展示了通过虚函数实现的多态性,即相同的函数调用可以根据对象的实际类型调用不同的实现。
通过使用虚函数和多态性,我们可以通过基类的指针或引用来处理不同类型的派生类对象,实现统一的接口和行为,提高代码的可扩展性和灵活性。
5、c++面向对象的特性?
C++ 面向对象的三大特征是:封装、继承、多态。
封装是C++面向对象编程的重要概念之一。它是指将数据和对数据的操作(方法或函数)封装在一个单独的实体中,形成一个类(Class)。类将数据和操作封装在一起,对外部世界隐藏了实现细节,只暴露了必要的接口。它通过隐藏实现细节、保护数据和提供抽象接口等方式,提供了安全性、可维护性、扩展性和抽象性等优势。
继承是一种面向对象编程的重要特性,它允许一个类(称为子类或派生类)从另一个类(称为父类或基类)继承属性和行为。通过继承,子类可以获得父类的成员变量和成员函数,同时可以扩展或修改这些成员以满足自身的需求。
继承概念的实现方式有两类:
实现继承:实现继承是指直接使用基类的属性和方法而无需额外编码的能力。
接口继承:接口继承是指仅使用属性和方法的名称、但是子类必需提供实现的能力。
多态(polymorphism)是面向对象编程的重要概念之一。它允许使用基类的指针或引用来引用派生类的对象,并在运行时根据实际对象的类型来调用相应的成员函数。
多态与非多态的实质区别就是函数地址是早绑定还是晚绑定的。如果函数的调用,在编译器编译期间就可以确定函数的调用地址,并产生代码,则是静态的,即地址早绑定。而如果函数调用的地址不能在编译器期间确定,需要在运行时才确定,这就属于晚绑定。
6、什么是虚函数、纯虚函数和抽象类?它们有什么区别?
虚函数(Virtual Function):
- 虚函数是在基类中声明的带有关键字
virtual
的成员函数。 - 它允许在派生类中重写(override)基类的函数,并在运行时根据对象的实际类型调用正确的函数版本。
- 虚函数通过在基类中使用
virtual
关键字来声明,并使用派生类中相同的函数名和参数列表进行重写。
纯虚函数(Pure Virtual Function):
- 纯虚函数是在基类中声明的没有实现(没有函数体)的虚函数。
- 它通过在基类中使用
= 0
来声明,表示该函数没有默认实现,需要在派生类中进行实现。 - 包含纯虚函数的类称为抽象类,抽象类不能被实例化。
抽象类(Abstract Class):
- 抽象类是包含至少一个纯虚函数的类。
- 抽象类不能被实例化,只能作为基类来派生其他类。
- 派生类必须实现基类中的纯虚函数,才能被实例化。
区别:
- 虚函数可以在基类和派生类中都有实现,但纯虚函数只能在基类中声明而不能有实现。
- 抽象类是包含至少一个纯虚函数的类,它不能被实例化,而非抽象的基类和派生类可以被实例化。
- 派生类必须实现基类的纯虚函数,而对于虚函数没有这个要求。
这个问题可以进一步扩展,让面试者解释虚函数和纯虚函数的用途以及在实际开发中的应用场景,以考察他们对多态性和抽象类的理解。
7、介绍一下C++ 中的指针参数传递?
指针参数传递本质上是值传递
在C++中,指针参数传递是一种常见的参数传递方式。通过传递指针作为函数的参数,可以实现对函数外部变量的修改,以及在函数内部对变量的引用和操作。
指针参数传递的特点如下:
-
传递指针的地址:在函数声明时,参数类型为指针类型,例如
int*
、float*
等。传递给函数的是指针的地址,而不是指针指向的具体数据。 -
修改实际参数:通过指针参数传递,可以在函数内部修改指针所指向的数据。因为传递的是指针的地址,函数可以通过解引用指针来修改指向的数据。
-
传递空指针:指针参数也可以传递空指针(nullptr),表示没有指向有效数据的指针。
下面是一个简单的示例代码,演示了指针参数传递的用法:
#include <iostream> void increment(int* ptr) (*ptr)++; // 通过解引用指针修改数据 void swap(int* a, int* b) int temp = *a; *a = *b; *b = temp; int main() int num1 = 5; int num2 = 10; increment(&num1); // 传递指针的地址 std::cout << "num1 after increment: " << num1 << std::endl; swap(&num1, &num2); // 传递指针的地址 std::cout << "num1 after swap: " << num1 << std::endl; std::cout << "num2 after swap: " << num2 << std::endl; return 0;
在上面的示例中,increment()
函数接受一个指针参数ptr
,通过解引用指针来递增所指向的数据。swap()
函数接受两个指针参数a
和b
,通过解引用指针交换它们所指向的数据。
通过指针参数传递,可以修改函数外部的变量,并实现函数间的数据共享。指针参数传递在处理大型数据结构时尤为有用,因为它避免了复制整个数据结构的开销,并允许对数据进行原地修改。但需要注意,使用指针参数传递时需要确保指针的有效性,避免空指针和悬空指针的问题。
8、介绍一下C++ 中的引用参数传递
在C++中,引用参数传递是一种通过引用来传递参数的方法。通过引用参数传递,可以在函数内部直接操作原始变量,而无需进行副本的复制。这种传递方式可以用于函数的输入参数、输出参数或输入输出参数。
使用引用参数传递时,函数的参数声明中使用引用符号(&)来指示参数是一个引用。以下是一个使用引用参数传递的简单示例:
void swap(int& a, int& b) int temp = a; a = b; b = temp; int main() int x = 5; int y = 10; cout << "Before swap: x = " << x << ", y = " << y << endl; swap(x, y); cout << "After swap: x = " << x << ", y = " << y << endl; return 0;
在上面的示例中,swap
函数使用引用参数传递来交换两个整数变量的值。通过将a
和b
声明为引用类型,函数可以直接修改main
函数中的x
和y
变量,而不需要通过指针或返回值来实现。
引用参数传递还可以用于改变传入函数的参数的值。例如:
void increment(int& value) value++; int main() int num = 5; cout << "Before increment: " << num << endl; increment(num); cout << "After increment: " << num << endl; return 0;
在上面的示例中,increment
函数通过引用参数传递,将num
的值递增了1。由于函数中使用的是引用,num
的值在函数内部被修改,并在函数调用后保持了修改后的值。
需要注意的是,引用参数传递并不创建新的变量,而是将原始变量与函数参数建立了别名关系。因此,函数对引用参数的修改会直接反映到原始变量上。此外,引用参数传递也可以提高性能,因为不需要进行副本的复制操作。
总结而言,引用参数传递是一种在C++中使用引用来传递参数的方法,可以实现对原始变量的直接修改,以及避免副本的创建和复制。
9、C++ 中重载和重写的区别?
-
重载(Overloading): 重载是指在同一个作用域中定义多个具有相同名称但参数列表不同的函数或运算符。重载函数可以根据不同的参数类型或参数个数,提供不同的实现方式。编译器根据函数调用时的参数匹配来确定调用哪个重载函数。重载可以应用于普通函数、类的成员函数和运算符重载等场景。
示例:
void print(int num) cout << "Printing integer: " << num << endl; void print(float num) cout << "Printing float: " << num << endl; int main() print(5); print(3.14f); return 0;
-
重写(Overriding): 重写是指在派生类中重新实现基类的虚函数,以改变其行为。基类中的虚函数被声明为
virtual
,派生类中使用相同的函数名、参数列表和返回类型来重写该函数。在运行时,根据对象的实际类型来调用对应的重写函数。重写函数必须具有相同的函数签名(函数名、参数列表和返回类型)。示例:
class Shape public: virtual void draw() cout << "Drawing shape" << endl; ; class Circle : public Shape public: void draw() override cout << "Drawing circle" << endl; ; int main() Shape* shape = new Circle(); shape->draw(); // 调用Circle类中的重写函数 delete shape; return 0;
总结:
- 重载(Overloading)是在同一个作用域中定义多个具有相同名称但参数列表不同的函数或运算符。
- 重写(Overriding)是在派生类中重新实现
10、虚函数相关(虚函数表,虚函数指针),虚函数的实现原理
首先我们来说一下,C++中多态的表象,在基类的函数前加上 virtual 关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数,如果是基类,就调用基类的函数。
实际上,当一个类中包含虚函数时,编译器会为该类生成一个虚函数表,保存该类中虚函数的地址,同样,派生类继承基类,派生类中自然一定有虚函数,所以编译器也会为派生类生成自己的虚函数表。当我们定义一个派生类对象时,编译器检测该类型有虚函数,所以为这个派生类对象生成一个虚函数指针,指向该类型的虚函数表,这个虚函数指针的初始化是在构造函数中完成的。
11、解释C++中的浅拷贝和深拷贝的概念,并给出它们的区别。
在C++中,浅拷贝(Shallow Copy)和深拷贝(Deep Copy)是两种对象复制的方式。
浅拷贝是指将一个对象的值复制到另一个对象,包括对象中的数据成员和指针成员的值。在浅拷贝中,两个对象将共享相同的内存区域,它们的指针成员将指向同一块内存。这意味着当一个对象修改了共享的资源时,另一个对象也会受到影响。这可能导致意外的行为和资源管理问题。
深拷贝是指创建一个新的对象,将原对象的值复制到新对象中。在深拷贝中,每个对象都有自己独立的内存区域,包括指针成员指向的内存。这样,当一个对象修改自己的资源时,不会影响其他对象。深拷贝需要分配新的内存,并将原对象的值复制到新内存中,确保对象之间的独立性。
区别:
- 在浅拷贝中,两个对象共享相同的内存,包括指针成员指向的内存。而深拷贝中,每个对象都有自己独立的内存。
- 在浅拷贝中,当一个对象修改共享资源时,另一个对象也会受到影响。而在深拷贝中,每个对象修改自己的资源,不会影响其他对象。
- 浅拷贝只复制数据本身,而不复制指针指向的内容。而深拷贝会为指针指向的内容分配新的内存,并将原对象的值复制到新内存中。
对于包含动态分配内存或资源管理的类,通常需要使用深拷贝来避免共享资源和潜在的错误。可以通过自定义拷贝构造函数和赋值运算符重载来实现深拷贝。
#include <iostream> #include <cstring> // 用于字符串操作的头文件 class String public: String(const char* str = "") size = std::strlen(str); data = new char[size + 1]; std::strcpy(data, str); // 拷贝构造函数 String(const String& other) size = other.size; data = new char[size + 1]; std::strcpy(data, other.data); // 赋值运算符重载 String& operator=(const String& other) if (this != &other) delete[] data; size = other.size; data = new char[size + 1]; std::strcpy(data, other.data); return *this; ~String() delete[] data; void print() std::cout << data << std::endl; private: char* data; size_t size; ; int main() String s1("Hello"); String s2 = s1; // 浅拷贝,共享相同的内存 s1.print(); // 输出:Hello s2.print(); // 输出:Hello // 修改 s1 s1 = "World"; s1.print(); // 输出:World s2.print(); // 输出:Hello(仍然指向原来的内存,受到影响) return 0;
在上述示例中,我们定义了一个 String
类,它有一个指针成员 data
用于存储字符串内容,以及一个 size
成员表示字符串长度。在构造函数、拷贝构造函数和赋值运算符重载中,我们使用深拷贝来为每个对象分配独立的内存,并复制字符串内容。
在 main
函数中,我们创建了两个对象 s1
和 s2
,并将 s1
的值赋给 s2
。由于没有显式定义拷贝构造函数,这里会调用默认的浅拷贝行为。这导致 s1
和 s2
共享相同的内存区域,即它们的指针成员 data
指向同一块内存。
当我们修改 s1
的值为 "World" 时,它会重新分配内存,并将新的字符串复制到新分配的内存中。但是,s2
仍然指向原来的内存区域,所以它的值仍然是 "Hello"。这就展示了浅拷贝的问题:修改一个对象的值会影响到其他对象。
12、什么是析构函数(Destructor)?它的作用是什么?为什么我们需要使用析构函数?
在C++中,析构函数是一种特殊的成员函数,用于在对象生命周期结束时执行清理和资源释放的操作。析构函数的名称与类名相同,前面加上一个波浪号(~)作为前缀。
析构函数的作用是确保对象在销毁时能够正确释放动态分配的资源、关闭打开的文件、释放锁定的资源等。它通常与构造函数成对出现,构造函数负责对象的初始化,而析构函数则负责对象的清理。
我们需要使用析构函数的主要原因是避免资源泄漏。当一个对象的生命周期结束时,如果没有适当的析构函数来清理资源,可能会导致内存泄漏、文件句柄未关闭或其他资源泄漏的问题。
以下是一个简单的示例,展示了如何定义和使用析构函数:
#include <iostream> class MyClass public: MyClass() std::cout << "Constructor called" << std::endl; ~MyClass() std::cout << "Destructor called" << std::endl; ; int main() MyClass obj; // 创建一个对象 // 执行一些操作 return 0; // 对象销毁时,析构函数被调用
在上述示例中,MyClass
类定义了一个构造函数和析构函数。当创建一个MyClass
对象时,构造函数被调用,输出 "Constructor called"。在main
函数结束时,对象的生命周期结束,析构函数被调用,输出 "Destructor called"。
总结:析构函数是用于在对象销毁时执行清理和资源释放操作的特殊成员函数。它的作用是避免资源泄漏,并与构造函数成对出现,确保对象的完整生命周期得到管理和控制。
13、请解释什么是动态绑定(Dynamic Binding)以及静态绑定(Static Binding),它们之间有何区别?
动态绑定和静态绑定是面向对象编程中的两种不同的函数调用机制。
静态绑定是指在编译时确定要调用的函数,它是根据变量的静态类型来决定调用哪个函数。静态类型是在编译时已知的类型,它是变量声明时指定的类型。静态绑定通过编译器在编译时解析函数调用,将函数地址与函数调用绑定在一起。静态绑定通常用于非虚函数和静态成员函数的调用。
动态绑定是指在运行时确定要调用的函数,它是根据变量的实际类型来决定调用哪个函数。实际类型是在运行时才能确定的类型,它是变量所引用或指向的对象的类型。动态绑定通过虚函数表(Virtual Function Table)来实现。通过虚函数表指针(vptr)和虚函数表的地址,程序能够在运行时动态地查找并调用正确的虚函数。
区别:
- 静态绑定在编译时确定要调用的函数,而动态绑定在运行时确定要调用的函数。
- 静态绑定是根据变量的静态类型来决定调用哪个函数,而动态绑定是根据变量的实际类型来决定调用哪个函数。
- 静态绑定适用于非虚函数和静态成员函数的调用,而动态绑定适用于虚函数的调用。
动态绑定使得通过基类指针或引用调用虚函数时,可以根据对象的实际类型来调用适当的函数,实现多态性的特性。它是实现运行时多态的关键机制,允许程序在运行时动态地适应不同的对象类型。
14、什么是智能指针?它的作用是什么?请举例说明使用智能指针的好处。
智能指针是C++中的一种对象,它行为类似于指针,但具有自动化的内存管理功能。它们用于管理动态分配的内存资源,确保在不需要时正确释放内存,避免内存泄漏。
智能指针的主要作用是提供自动内存管理,减少手动内存管理的复杂性和错误。它们能够自动在适当的时机调用析构函数来释放所管理的资源,无需手动调用 delete
操作符。
以下是一个示例,说明使用智能指针的好处:
#include <iostream> #include <memory> class MyClass public: MyClass() std::cout << "Constructor called" << std::endl; ~MyClass() std::cout << "Destructor called" << std::endl; void doSomething() std::cout << "Doing something" << std::endl; ; int main() std::unique_ptr<MyClass> ptr(new MyClass()); // 使用智能指针创建对象 ptr->doSomething(); // 调用对象的成员函数 // 在离开作用域时,智能指针会自动释放内存,调用析构函数 return 0;
在上述示例中,使用 std::unique_ptr
创建了一个 MyClass
对象的智能指针 ptr
。当 ptr
离开作用域时,它会自动释放所管理的内存,调用析构函数。这样就避免了手动调用 delete
操作符,确保内存的正确释放。
使用智能指针的好处包括:
- 自动内存管理:智能指针会在适当的时机自动释放内存,无需手动管理内存资源,避免内存泄漏和悬挂指针等问题。
- 异常安全性:智能指针能够处理异常情况,确保在发生异常时正确释放已分配的内存。
- 扩展性:C++标准库提供了多种智能指针类型(如
std::unique_ptr
、std::shared_ptr
、std::weak_ptr
),可以根据需要选择合适的智能指针类型,满足不同的内存管理需求。
总结:智能指针是一种能够自动管理动态分配内存的指针,它提供了自动内存管理、异常安全性和灵活的扩展性。使用智能指针可以简化内存管理的复杂性,提高代码的可靠性
15、CPP智能指针有哪几种?
- auto_ptr
- unique_ptr
- shared_ptr
- weak_ptr
16、介绍一下构造函数?
类的对象被创建时,编译系统为对象分配内存空间,并自动调用构造函数,由构造函数完成成员的初始化工作。
即构造函数的作用:初始化对象的数据成员。
无参数构造函数: 即默认构造函数,如果没有明确写出无参数构造函数,编译器会自动生成默认的无参数构造函数,函数为空,什么也不做,如果不想使用自动生成的无参构造函数,必需要自己显示写出一个无参构造函数。
一般构造函数: 也称重载构造函数,一般构造函数可以有各种参数形式,一个类可以有多个一般构造函数,前提是参数的个数或者类型不同,创建对象时根据传入参数不同调用不同的构造函数。
拷贝构造函数: 拷贝构造函数的函数参数为对象本身的引用,用于根据一个已存在的对象复制出一个新的该类的对象,一般在函数中会将已存在的对象的数据成员的值一一复制到新创建的对象中。如果没有显示的写拷贝构造函数,则系统会默认创建一个拷贝构造函数,但当类中有指针成员时,最好不要使用编译器提供的默认的拷贝构造函数,最好自己定义并且在函数中执行深拷贝。
类型转换构造函数: 根据一个指定类型的对象创建一个本类的对象,也可以算是一般构造函数的一种,这里提出来,是想说有的时候不允许默认转换的话,要记得将其声明为 explict 的,来阻止一些隐式转换的发生。
赋值运算符的重载 :注意,这个类似拷贝构造函数,将=右边的本类对象的值复制给=左边的对象,它不属于构造函数,=左右两边的对象必需已经被创建。如果没有显示的写赋值运算符的重载,系统也会生成默认的赋值运算符,做一些基本的拷贝工作。
17、简述指针和引用的区别
指针和引用是C++中用于处理变量间关系的两个重要概念,它们有以下区别:
-
定义方式和语法:指针使用
*
来声明和操作,引用使用&
来声明和操作。- 指针的声明:
int* ptr;
- 引用的声明:
int& ref = variable;
- 指针的声明:
-
内存地址和别名:指针保存的是变量的内存地址,而引用是变量的别名。
- 指针:指针变量存储了另一个变量的地址,可以通过解引用操作符
*
来访问指针指向的变量。 - 引用:引用本身并不占据额外的内存空间,它只是已存在变量的别名,对引用的操作就是对原始变量的操作。
- 指针:指针变量存储了另一个变量的地址,可以通过解引用操作符
-
空值和未初始化:指针可以为空值(nullptr)或未初始化,引用必须在声明时初始化,并且不能为空。
- 指针:可以被赋值为
nullptr
,表示空指针或无效指针。 - 引用:必须在声明时初始化,并且不能重新绑定到其他变量。
- 指针:可以被赋值为
-
可变性:指针可以被重新赋值,指向不同的对象,而引用一旦绑定到一个对象后,就不能再改变其引用的对象。
- 指针:可以通过赋值操作改变指针的指向,可以指向不同的对象。
- 引用:一旦绑定到一个对象,就无法重新绑定到其他对象。
-
空指针和空引用:指针可以是空指针(nullptr),引用不能为空。
- 指针:可以指向空地址,即空指针,表示指向的对象不存在。
- 引用:必须引用一个有效的对象,不能引用空值。
-
函数参数传递方式:指针可以作为函数参数传递,引用也可以作为函数参数传递。
- 指针:可以通过传递指针来修改实际参数的值。
- 引用:通过引用传递可以实现对实际参数的修改,且不需要使用指针操作符。
需要注意的是,指针和引用在使用时具有不同的语义和行为,正确理解它们的区别对于正确使用它们是非常重要的。选择使用指针还是引用取决于具体的需求和编程场景。
18、new / delete ,malloc / free 区别?
new
/ delete
和 malloc
/ free
是在C++中用于动态内存分配和释放的两组操作符,它们之间有以下区别:
-
类型安全性:
new
/delete
是C++的操作符,对于对象的构造和析构函数进行正确的调用,保证了类型的安全性。而malloc
/free
是C的函数,只负责分配和释放内存块,不涉及对象的构造和析构函数的调用。 -
内存大小计算:
new
操作符根据类型信息自动计算所需内存大小,无需手动指定。而malloc
函数需要显式指定所需内存大小。 -
返回类型:
new
返回的是所分配对象的类型指针,即指向分配的对象。而malloc
返回的是void*
指针,需要进行显式的类型转换。 -
构造和析构函数的调用:
new
操作符在分配内存后会自动调用对象的构造函数进行初始化,而delete
操作符在释放内存前会自动调用对象的析构函数进行清理。而malloc
/free
不会自动调用对象的构造和析构函数。 -
重载支持:
new
/delete
可以通过重载实现自定义的内存管理行为,例如重载new
操作符以实现自定义的内存分配策略。而malloc
/free
不支持重载。 -
异常处理:
new
在分配内存时,如果失败会抛出std::bad_alloc
异常。而malloc
在分配内存失败时返回空指针。
总的来说,new
/ delete
是C++中推荐使用的动态内存管理方式,它们提供了更高的类型安全性、对象构造和析构函数的自动调用等特性。而 malloc
/ free
在C和C++兼容的代码中仍然可以使用,但需要注意手动管理对象的构造和析构函数的调用,并且需要显式指定内存大小。
19、
20、
21、
22、
23、
24、
25、
26、
27、
28、
以上是关于C++面试题的主要内容,如果未能解决你的问题,请参考以下文章