-------------- c++面试题-----------------
1、检查下面代码有什么问题?
void GetMemory( char *p )
{
p = (char *) malloc( 100 );
}
void Test( void )
{
char *str = NULL;
GetMemory( str );
strcpy( str, “hello world” );
printf( str );
}
函数存在段错误问题:
GetMemory 函数:该函数接受一个指向字符的指针 char *p 作为参数。在函数内部,它为100个字符分配内存空间,并将该内存块的地址赋给 p。然而,p 是一个局部变量,因此在函数内部更改其值不会影响从外部传递给它的指针。
Test 函数:它声明了一个指针 str,并将其初始化为 NULL。然后它调用 GetMemory,并将 str 作为参数传递。然而,由于 str 是按值传递的,GetMemory 内对 p 的任何更改都不会反映到 Test 中的 str 上。因此,在调用 GetMemory 后,str 仍然是 NULL。
然后调用 strcpy,并以 str 作为参数。这将导致未定义的行为,因为 str 是 NULL,你尝试将字符串 “hello world” 复制到一个空指针。
修改:
void GetMemory(char **p) {
*p = (char *)malloc(100);
}
void Test(void) {
char *str = NULL;
GetMemory(&str);
if (str != NULL) {
strcpy(str, "hello world");
printf("%s\n", str);
free(str); // 释放分配的内存
}
}
2、C和C++的区别
C和C++是两种不同的编程语言,虽然它们之间有许多共同之处,但也存在一些显著的区别。以下是C和C++之间的一些主要区别:
-
面向对象编程:
- C是一种过程式编程语言,它主要关注函数和过程的编写。C中没有类、对象和继承等面向对象编程的概念。
- C++是一种面向对象编程语言,它扩展了C语言,并引入了类、对象、继承、封装和多态等面向对象的特性。
-
类和对象:
- C中没有类和对象的概念,数据和函数是分离的,函数可以操作任何数据。
- C++引入了类和对象的概念,允许将数据和函数封装在一起,形成类,通过创建对象来访问类的成员。
-
函数重载和默认参数:
- C中不支持函数重载和默认参数的特性。
- C++支持函数重载(在同一作用域内可以定义多个同名函数,但参数列表不同)和默认参数(可以为函数参数提供默认值)。
-
命名空间:
- C中没有命名空间的概念,所有的全局标识符都处于相同的命名空间。
- C++引入了命名空间的概念,允许将标识符分组并放置在特定的命名空间中,以防止名称冲突。
-
异常处理:
- C中没有内置的异常处理机制,错误通常通过返回值或全局变量来表示。
- C++引入了异常处理机制,允许程序员使用
try
、catch
和throw
等关键字来处理异常情况。
-
标准库:
- C标准库提供了一系列函数和宏,用于实现常见的操作,如输入输出、字符串处理、内存管理等。
- C++标准库在C标准库的基础上进一步扩展,包括了STL(标准模板库)等组件,提供了丰富的容器、算法、迭代器等,以支持更加高级和现代化的编程。
3、C++11有哪些新特性?介绍一下
C++11引入了许多新特性,包括:
1. **自动类型推断(auto)**:
- `auto` 关键字允许编译器根据初始化表达式的类型推断变量的类型,使得代码更简洁、可读性更高。
```cpp
auto i = 10; // i的类型被推断为int
auto str = "Hello"; // str的类型被推断为const char*
```
2. **范围for循环**:
- `for` 循环的一种新形式,可以更方便地遍历容器、数组等序列。
```cpp
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto& elem : vec) {
std::cout << elem << " ";
}
```
3. **Lambda表达式**:
- Lambda表达式允许在代码中内联定义匿名函数,提供了一种更灵活的函数定义方式。
```cpp
auto add = [](int a, int b) { return a + b; };
std::cout << add(3, 4); // 输出7
```
4. **智能指针**:
- C++11引入了 `std::unique_ptr` 和 `std::shared_ptr` 等智能指针,用于自动管理动态分配的内存,避免了内存泄漏和悬挂指针等问题。
```cpp
std::unique_ptr<int> ptr = std::make_unique<int>(5);
```
5. **右值引用和移动语义**:
- 右值引用允许我们对临时对象(右值)进行引用,从而实现移动语义,提高了内存使用效率和性能。
```cpp
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1); // 移动v1的内容到v2
```
6. **nullptr**:
- `nullptr` 是一种新的空指针常量,用于替代传统的 `NULL` 宏,并提供更好的类型安全性。
```cpp
int* ptr = nullptr;
```
7. **enum类**:
- 枚举类(enum class)是一种强类型的枚举,它们的作用域被限制在类中,不会导出到外部作用域。
```cpp
enum class Color { Red, Green, Blue };
Color c = Color::Red;
```
4、虚函数是怎么实现的
在C++中,虚函数的实现是通过虚函数表(vtable)和虚函数指针(vptr)来实现的。
当一个类中声明了虚函数时,编译器会为该类生成一个虚函数表。虚函数表是一个指针数组,每个元素指向一个虚函数的实现。同时,编译器会在每个对象中添加一个指向虚函数表的虚函数指针。
当调用一个虚函数时,实际上是通过对象的虚函数指针来查找虚函数表中相应的函数指针,然后调用对应的函数。因此,通过虚函数的调用可以在运行时确定调用的是哪个函数版本,实现了多态性。
以下是虚函数的实现步骤:
1.当一个类中声明了虚函数时,编译器会为该类生成一个虚函数表(vtable)。
2.对象中会添加一个指向虚函数表的虚函数指针(vptr),用于在运行时查找虚函数。
3.每个继承自该类的派生类也会有自己的虚函数表,并覆盖父类中的虚函数。
4.当调用虚函数时,通过对象的虚函数指针找到虚函数表,然后根据函数在虚函数表中的偏移量找到相应的函数指针,最后调用该函数。
5. 堆和栈的区别
堆和栈是在计算机内存中用于存储数据的两种不同区域,它们有几个重要的区别:
-
分配方式:
- 栈:栈是一种自动分配和释放内存的数据结构,它是由编译器自动管理的。当一个函数被调用时,函数的局部变量和参数会被存储在栈上,当函数调用结束时,这些数据会自动被释放。
- 堆:堆是一种手动分配和释放内存的数据结构,它的分配和释放由程序员负责。在堆上分配内存需要调用特定的函数(如
malloc()
、new
等),并在不再需要时显式释放内存(如free()
、delete
等)。
-
管理机制:
- 栈:栈是一种后进先出(LIFO)的数据结构,只支持在栈顶进行插入和删除操作。由于栈的内存分配和释放是由编译器自动管理的,因此它具有较高的效率和较小的内存开销。
- 堆:堆是一种无序的、自由存储的数据结构,允许在任何位置进行内存的分配和释放。由于堆的内存管理由程序员负责,因此可能存在内存泄漏和内存碎片等问题。
-
生命周期:
- 栈:栈中存储的数据的生命周期由其作用域决定,当作用域结束时,栈中的数据会自动被销毁。
- 堆:堆中分配的内存的生命周期由程序员显式控制,需要手动调用相应的函数来释放分配的内存。
-
大小限制:
- 栈:栈的大小通常较小,并且由操作系统或编译器设置固定的上限。
- 堆:堆的大小通常较大,并且取决于系统的虚拟内存大小,可以根据需要动态增长。
-
生长方向:
- 栈:栈向下。
- 堆:堆向上。
堆和栈是两种不同的内存分配机制,它们分别适用于不同的场景,栈用于存储局部变量和函数调用信息等,而堆用于动态分配内存以存储动态数据结构和对象。
6. 重载overload,覆盖override,重写overwrite,这三者之间的区别?
重载(Overload):
重载是指在同一个作用域内定义多个名称相同但参数列表不同的函数的行为。这些函数可以有不同的参数类型、不同的参数个数或不同的参数顺序。
重载的目的是为了提高代码的可读性和灵活性,使得函数名可以根据不同的参数类型或个数执行不同的操作。
覆盖(Override):
覆盖是指子类(派生类)重新定义了父类(基类)中的虚函数,使用相同的函数签名(名称、参数列表和返回类型)。
覆盖允许子类提供其自己的实现,以覆盖父类中的实现。在运行时,根据对象的实际类型来调用相应的函数。
重写(Redefine 或 Overwrite):
重写可以指覆盖,但通常更广泛地用于任何情况下重新定义一个已有的函数,不论该函数是否是虚函数(与覆盖的区别)。
重写的行为可以包括函数签名的改变、添加或删除参数,以及函数体的改变。
7. 介绍一下四种智能指针
在C++中,智能指针是一种用于管理动态分配内存的智能化工具,它们可以自动地处理内存的分配和释放,从而避免了内存泄漏和悬挂指针等问题。C++标准库提供了四种主要的智能指针,它们分别是:
std::unique_ptr:
std::unique_ptr 用于管理单个对象的所有权,即每个对象只能由一个 std::unique_ptr 持有。它是独占式拥有(exclusive ownership)的智能指针,不能进行复制,但可以进行移动操作。
当 std::unique_ptr 被销毁时,它会自动释放其所指向的对象。
std::shared_ptr:
std::shared_ptr 允许多个智能指针共享同一个对象的所有权,通过引用计数来管理资源。它是共享式拥有(shared ownership)的智能指针,可以进行复制和移动操作。
引用计数会在每个 std::shared_ptr 指向同一对象时递增,当所有指向该对象的 std::shared_ptr 都销毁时,对象会被释放。
std::weak_ptr:
std::weak_ptr 是一种弱引用智能指针,它可以指向 std::shared_ptr 指向的对象,但不会增加引用计数。因此,它不会影响对象的生命周期,也不会阻止对象被释放。
std::weak_ptr 主要用于解决 std::shared_ptr 的循环引用问题,它允许引用的对象被释放,同时避免了悬挂指针的问题。
std::auto_ptr(C++11之前的标准):
std::auto_ptr 是早期版本的智能指针,用于独占式拥有资源。它的使用受到了很多限制,且已经被废弃。在现代C++中,推荐使用 std::unique_ptr 来代替 std::auto_ptr。
这四种智能指针各自适用于不同的场景,选择合适的智能指针可以提高代码的可维护性和安全性,同时避免内存管理的问题。通常情况下,推荐优先使用 std::unique_ptr(应为不需要引用计数开销少) 和 std::shared_ptr,而在需要解决循环引用问题时使用 std::weak_ptr。
8.什么是野指针,如何避免
野指针是指指向未知内存地址或已经释放的内存地址的指针。使用野指针可能会导致程序崩溃、内存泄漏或未定义的行为,因为这些指针所指向的内存区域可能已经被其他程序使用或操作系统回收。
要避免野指针,可以采取以下几种方法:
-
初始化指针:始终在声明指针后立即初始化,将其指向一个有效的内存地址或者设为 nullptr(C++11之后的标准)。
int* ptr = nullptr; // 初始化为 nullptr
-
避免悬空指针:在释放内存后,将指针设为 nullptr,以避免悬空指针。
delete ptr; ptr = nullptr;
-
谨慎使用指针:尽量避免手动管理内存,使用智能指针(如
std::unique_ptr
、std::shared_ptr
)或容器(如std::vector
、std::string
)等自动管理内存的机制来替代裸指针。std::unique_ptr<int> ptr = std::make_unique<int>(5); // 使用智能指针
-
避免指针的悬挂和重复释放:确保指针在合适的时候释放,不要在指针还在使用时就释放它,也不要多次释放同一个指针。
-
规范化指针的生命周期:明确指针的生命周期,避免指针在超出其作用域后仍然被使用。
9、new和malloc的区别
new 是 C++ 中的关键字,malloc() 是 C 标准库函数。
new 在分配内存后会调用对象的构造函数进行初始化。如果是数组,每个对象都会被初始化。
malloc() 只是简单地分配一块内存,并不会调用构造函数进行对象的初始化。
new 更符合 C++ 的面向对象特性,提供了更方便的内存分配和对象初始化方式,而 malloc() 则更适用于 C 语言或者需要分配未初始化内存块的场景。
10、列举一些常用的shell命令
当使用UNIX shell时,有许多常用的命令可用于执行各种任务。以下是一些常见的shell命令:
-
文件和目录操作:
ls
:列出当前目录下的文件和子目录。cd
:切换当前工作目录。pwd
:显示当前工作目录的路径。mkdir
:创建新目录。rm
:删除文件或目录。cp
:复制文件或目录。mv
:移动文件或目录。
-
文本处理:
cat
:连接文件并打印到标准输出。grep
:在文件中查找匹配的文本。sed
:流编辑器,用于处理和转换文本。awk
:文本处理工具,可以逐行处理文本并执行指定的操作。
-
系统信息:
top
:显示系统中正在运行的进程和系统资源的使用情况。ps
:显示当前进程的状态。df
:显示磁盘空间使用情况。free
:显示系统内存使用情况。
-
压缩和解压缩:
tar
:用于创建和提取 tar 归档文件。gzip
:用于压缩文件。gunzip
:用于解压缩文件。
-
权限管理:
chmod
:更改文件或目录的权限。chown
:更改文件或目录的所有者。chgrp
:更改文件或目录的所属组。
-
网络工具:
ping
:用于测试与另一个主机的连接。ifconfig
或ip
:显示和配置网络接口信息。netstat
:显示网络连接、路由表和网络接口信息。
-
包管理(仅限某些系统):
apt
或apt-get
:用于Debian和Ubuntu系统的软件包管理。yum
:用于CentOS和Fedora系统的软件包管理。
-
进程管理:
kill
:终止进程。ps
:显示进程的状态。nice
:调整进程的优先级。
11、怎么去定义一个只在堆或者只在栈上生成的类。
只能在堆上生成的类:
使用 delete 关键字删除默认构造函数
,防止在栈上生成对象。同时,将构造函数声明为私有的
,以防止外部直接调用构造函数。为了在堆上生成对象,提供一个静态成员函数 create(),该函数返回在堆上创建的对象指针。
class HeapOnlyClass {
public:
// 禁止默认构造函数
HeapOnlyClass() = delete;
// 析构函数
~HeapOnlyClass() {
// 打印消息确认对象是否在堆上被销毁
std::cout << "Destructor called for HeapOnlyClass" << std::endl;
}
// 静态成员函数,返回堆上生成的对象
static HeapOnlyClass* create() {
return new HeapOnlyClass();
}
private:
// 私有化构造函数,防止在栈上生成对象
HeapOnlyClass() {}
};
只能在栈上生成的类:
通过将 operator new 和 operator delete 私有化,禁止在堆上生成对象
。这样,在编译时如果有代码尝试在堆上生成对象,就会报错。因为这个类没有提供静态成员函数来在堆上生成对象,所以只能在栈上生成对象。
class StackOnlyClass {
public:
// 构造函数
StackOnlyClass() {
// 打印消息,确认对象是否在栈上被创建
std::cout << "Constructor called for StackOnlyClass" << std::endl;
}
// 析构函数
~StackOnlyClass() {
// 在析构函数中加入一些逻辑,以确认对象是否在栈上被销毁
std::cout << "Destructor called for StackOnlyClass" << std::endl;
}
private:
// 禁止在堆上生成对象
void* operator new(size_t) = delete;
void operator delete(void*) = delete;
};
11、怎么做到线程同步。
线程同步通常通过互斥锁(Mutex)、条件变量(Condition Variable)、原子操作(Atomic Operations)等来实现。这些机制可以确保在多线程环境下对共享资源的安全访问和操作。
互斥锁(Mutex):
互斥锁是最常用的线程同步机制之一。它保证了在同一时间只有一个线程可以访问共享资源,其他线程必须等待锁释放后才能访问。在C++中,可以使用 std::mutex 来创建互斥锁。
条件变量(Condition Variable):
条件变量用于线程之间的通信和同步。它允许一个线程等待特定条件的发生,而其他线程可以通过通知来满足这个条件。在C++中,可以使用 std::condition_variable 来创建条件变量。
原子操作(Atomic Operations):
原子操作是一种不可分割的操作,可以保证在多线程环境下对共享变量的安全访问。它可以确保操作的原子性,即在执行过程中不会被其他线程中断。在C++中,可以使用 std::atomic 来创建原子变量。
几种常见的锁和它们的区别:
互斥锁(Mutex):
互斥锁用于保护共享资源,一次只允许一个线程访问共享资源,其他线程需要等待锁的释放才能访问。
只有获得锁的线程可以进入临界区,其他线程会被阻塞,直到锁被释放。读写锁(Read-Write Lock):
读写锁允许多个线程同时读取共享资源,但只允许一个线程进行写操作。
当一个线程要进行写操作时,其他线程都会被阻塞,直到写操作完成。自旋锁(Spin Lock):
自旋锁是一种简单的锁,它在等待锁的时候不会休眠,而是通过循环不停地检查锁的状态。
当获取锁的线程释放锁时,其他线程就可以尝试获取锁。递归锁(Recursive Lock):
递归锁允许同一个线程多次获取同一个锁,而不会造成死锁。
当一个线程多次获取锁时,必须相应地多次释放锁才能真正释放锁。条件变量(Condition Variable):
条件变量用于线程之间的通信和同步,一个线程等待特定的条件达成,而其他线程负责满足这个条件。
条件变量通常与互斥锁一起使用,等待条件时会释放互斥锁,条件满足后再重新获取互斥锁。
12、构造函数为什么不能作为虚函数。
-
构造函数不能声明为虚函数的主要原因是,在构造对象时,虚函数的实现依赖于对象的类型信息。然而,在调用构造函数时,对象的类型信息尚未完全确定,因为构造函数负责初始化对象的类型。因此,虚函数的概念在构造函数的上下文中不适用。
-
更具体地说,构造函数在创建对象时首先被调用,此时对象的虚函数表(vtable)还未构建,因此无法正确地解析虚函数调用。虚函数表存储了每个虚函数的地址,以便在运行时进行动态分派。而构造函数在对象的构造期间被调用,而非运行时,因此无法进行动态分派。
-
此外,将构造函数声明为虚函数也会导致一些潜在的问题,比如可能会引入对象的不一致状态或者无法正确地进行析构。因此,C++语言规范将构造函数设定为不能是虚函数。
-
虚函数的调用是在运行时动态决定的,而构造函数的调用是在编译时静态确定的。这就是为什么构造函数不能声明为虚函数的主要原因。
===== 笔试 ====
【实现一个反转链表】
struct node {
int data;
struct node *p;
};
void relist(list *t) {
if (t == NULL || t->first == NULL || t->first->next == NULL) {
// 空链表或只有一个元素的情况,无需反转
return;
}
node *a = t->first;
node *b = a->next;
node *c = NULL;
a->next = NULL; // 将第一个节点的下一个节点设置为NULL,因为在反转后它将成为最后一个节点
while (b != NULL) {
c = b->next; // 存储下一个节点
b->next = a; // 反转指针
a = b; // 将指针向前移动一步
b = c;
}
// 现在 'a' 指向最后一个节点(原先是第二个节点)
// 更新链表的 'first' 指针,使其指向新的第一个节点
t->first = a;
}