问题来源: https://www.zhihu.com/question/290102232/answer/2586920294
1. 信号的生命周期?
“信号有一个非常明确的生命周期。首先,产生(raise)信号(有时也说信号被发出或生成)。然后内核存储信号,直到可以发送该信号。最后,一旦有空闲,内核就会适当地处理信号。根据进程的请求,内核会执行以下三种操作之一: 忽略信号 不采取任何操作。但是有两种信号不能被忽略:SIGKILL和SIGSTOP。这样做的原因是系统管理员需要能够杀死或停止进程,如果进程能够选择忽略SIGKILL(使进程不能被杀死)或SIGSTOP(使进程不能被停止)将破坏这一权力。 捕获并处理信号 内核会暂停该进程正在执行的代码,并跳转到先前注册过的函数。接下来进程会执行这个函数。一旦进程从该函数返回,它会跳回到捕获信号的地方继续执行。 经常捕获的两种信号是SIGINT和SIGTERM。进程捕获SIGINT来处理用户产生的中断符——例如终端能捕获该信号并返回到主提示符。进程捕获SIGTERM以便在结束前执行必要的清理工作,例如断开网络,或删除临时文件。SIGKILL和SIGSTOP不能被捕获。 执行信号的默认操作 该操作取决于被发送的信号。默认操作通常是终止进程。例如,对SIGKILL来说就是这样。然而,许多信号是为在特定情况下的特殊目的而提供的,这些信号在默认的情况下是被忽略的,因为很多程序不关心它们。我们将很快介绍各种信号和及其默认操作。 过去,当信号被发送后,处理信号的函数只知道出现了某个信号,除此之外,对发生了什么情况却是一无所知。现在,内核能给接收信号的编程人员提供大量的上下文信息。正如我们后面将看到的,信号甚至能传递用户定义的数据。”
摘录来自 Linux系统编程(第2版) [美]Robert Love 此材料可能受版权保护。
2. 信号的产生方式?
Linux信号可以通过以下方式产生:
用户输入:例如在终端中按下ctrl+c会发送SIGINT(中断)信号。
系统事件:例如子进程完成,会发送SIGCHLD(子进程退出)信号。
软件错误:例如访问非法内存地址,会发送SIGSEGV(段错误)信号。
显式发送信号:例如使用kill命令向指定进程发送信号。
硬件异常:例如除零错误,会发送SIGFPE(浮点异常)信号。
定时器到期:例如使用alarm或setitimer函数设置定时器,当定时器到期时会发送SIGALRM(定时器到期)信号。
其他情况:例如进程收到CTRL+\键,会发送SIGQUIT(退出)信号。
总之,信号是一种异步通知机制,用于处理各种事件和异常情况。在Linux系统中,信号起着非常重要的作用,因为它们允许不同进程之间进行通信,同时还提供了异常处理机制。
3. 信号的处理方式?
Linux信号的处理方式分为3种:忽略、捕获和默认动作。
忽略信号:如果将一个信号设置为被忽略,当收到该信号时,系统不会采取任何措施。这对一些不需要处理的信号(例如SIGWINCH)非常有用。
捕获信号:可以使用signal或sigaction函数来指定一个信号的处理程序。当收到该信号时,操作系统将调用此处理程序来处理它。处理程序可以执行任何适当的操作,例如打印消息、修改全局变量等。
默认动作:每个信号都有一个默认动作。默认动作可能是终止进程、终止进程并生成核心转储文件、停止进程或什么都不做。通常通过改变信号的默认动作来处理信号是不安全的,因为可能会导致不可预测的行为。因此,使用捕获信号的方式进行信号处理更加可靠。
要处理信号,请使用signal或sigaction函数。这些函数允许您指定您希望对信号执行的操作。您还可以使用kill函数向其他进程发送信号。
4. 如何消除隐式转换?
在 C++ 中,可以通过使用关键字 explicit
来消除隐式转换。将构造函数声明为 explicit
表示该构造函数只能用于显式初始化,而不能进行隐式类型转换。
5. 重载、重写和隐藏的区别?
在 C++ 中,重载、重写和隐藏都是面向对象程序设计中的概念,它们的含义和使用方式有所不同。
重载(Overloading):指在同一个作用域内定义多个名称相同但参数列表不同的函数或者运算符。重载函数或运算符可以根据传递参数的类型和数量选择最匹配的版本进行调用。C++ 允许对大部分函数和运算符进行重载,包括构造函数、析构函数、赋值运算符等。
重写(Overriding):指在派生类中重新实现基类的虚函数,使得派生类的对象能够根据自己的需要提供更具体的实现。重写只适用于虚函数,因为只有虚函数才能在运行时根据对象的实际类型进行动态绑定。
隐藏(Hiding):指在派生类中定义了与基类同名的函数或变量,从而隐藏了基类的同名成员。如果派生类中没有显式地使用
using
关键字来引入基类的同名成员,则基类的同名成员在派生类中将无法直接访问。
下面是一个简单的例子:
#include
class Animal {
public:
virtual void makeSound() const {
std::cout "Animal is making a sound\n";
}
};
class Cat : public Animal {
public:
void makeSound() const override {
std::cout "Meow!\n";
}
};
class Lion : public Cat {
public:
void makeSound() const {
std::cout "Roar!\n";
}
};
int main() {
Animal* animal = new Animal;
animal->makeSound(); // Animal is making a sound
Cat* cat = new Cat;
cat->makeSound(); // Meow!
Animal* lion1 = new Lion;
lion1->makeSound(); // Roar!
Lion* lion2 = new Lion;
lion2->makeSound(); // Roar!
delete animal;
delete cat;
delete lion1;
delete lion2;
return 0;
}
在上述代码中,Animal
类定义了一个虚函数 makeSound()
,Cat
类重写了该函数,并提供了自己的实现。Lion
类继承了 Cat
类,它也重新实现了 makeSound()
函数,从而隐藏了基类 Cat
的同名成员。在程序中创建了不同类型的对象,并调用它们的 makeSound()
函数,可以看到不同类型的对象都有不同的输出结果。
总之,在 C++ 中,重载、重写和隐藏是面向对象编程中的重要概念。对于不同的需求,我们需要选择适当的方法来解决问题。
6. volatile表示什么?有什么作用?
在计算机编程中,volatile
是一个关键字,它用于修饰变量,表示该变量可能会被意外地修改。具体地说,volatile
告诉编译器,在对该变量进行优化时,不能假设该变量的值不会随时改变,从而确保程序可以正确地处理该变量。
通常情况下,编译器会对代码进行各种优化,比如将变量存储在寄存器中,以提高程序的执行效率。然而,这种优化可能会导致某些变量的值无法及时更新,因为编译器并不知道该变量可能被其他线程或外部事件修改。如果对这些变量使用volatile
修饰,则可以防止编译器对其进行一些不安全的优化,从而确保程序的正确性。
除了防止编译器优化外,volatile
还可以用于多线程编程中,例如在共享内存中访问变量时,由于多个线程可能同时访问该变量,因此需要使用volatile
关键字来确保每个线程都能看到最新的值,从而避免数据竞争等问题。
7. static_cast<>,dynamic_cast<>,const_cast<>,reinterpret_cast<>的各自作用和使用环境?
这四种类型转换是C++中的强制类型转换,具体作用和使用环境如下:
static_cast<>
static_cast<>
是一种静态类型转换,可以在编译时将一种类型转换为另一种类型。它可以用于常规类型之间的转换,例如将整数转换为浮点数或将指针转换为void指针。此外,static_cast<>
还可以用于转换指向基类的指针或引用为指向派生类的指针或引用。
dynamic_cast<>
dynamic_cast<>
是一种动态类型转换,可以在运行时确定类型转换是否有效。它通常用于将指向基类的指针或引用转换为指向派生类的指针或引用。如果转换失败(即指向的对象不是目标类型的实例),则返回一个空指针或引发一个std::bad_cast
异常。
const_cast<>
const_cast<>
是一种类型转换,用于去除变量的常量性质,从而允许修改其值。通常情况下,const_cast<>
用于将指向常量对象的指针或引用转换为指向非常量对象的指针或引用。需要注意的是,如果尝试通过const_cast<>
修改一个本来就是常量的对象,将导致未定义行为。
reinterpret_cast<>
reinterpret_cast<>
是一种类型转换,可以将任何指针或引用转换为另一种指针或引用。它通常用于将指针或引用转换为整数类型,以及在某些特殊情况下转换指针类型。需要注意的是,reinterpret_cast<>
不能保证转换后的指针或引用具有有效性,因此使用时需要格外小心。
总的来说,这四种类型转换都是C++中的强制类型转换,应该根据实际情况选择合适的类型转换操作。在进行类型转换时,需要注意类型兼容性和转换后的对象是否具有有效性等问题,避免出现未定义行为和安全风险。
8. malloc和new的区别?
malloc
和new
都可以用于动态分配内存,但它们有以下几个主要的区别:
malloc
是一个C标准库函数,而new
是一个C++关键字。因此,new
只能在C++中使用,而malloc
可以在C和C++中都使用。malloc
函数只负责分配内存空间,而不会初始化这块内存。new
操作符在分配内存之后还会调用类的构造函数,从而完成对象的初始化。new
操作符返回一个指向所分配内存的指针,并且根据类型进行了类型检查和类型转换,可以在编译时检查错误。而malloc
函数只返回一个void*
类型的指针,需要手动进行类型转换,容易出现类型错误。new
操作符在进行内存分配时,会考虑到类型的大小,并选择合适的内存对齐方式。而malloc
函数则只是简单地分配一块连续的内存空间,无法保证对齐方式。new
操作符提供了对数组的支持,可以方便地创建动态数组对象,而malloc
函数不能直接创建数组对象。
总的来说,相对于malloc
函数,new
操作符在使用上更为方便、安全、灵活,尤其在面向对象的程序设计中使用更为广泛。然而,对于一些特殊的场景或遗留的代码中,malloc
函数仍然是必不可少的工具之一。
9. free和delete的区别?
free
和 delete
都用于释放动态分配的内存,但它们之间存在着以下几个主要的区别:
free
是 C 标准库函数,而delete
是 C++ 关键字。因此,free
可以用于 C 和 C++ 中,而delete
只能在 C++ 中使用。free
只是简单地释放内存空间,不会调用对象的析构函数。而delete
会先调用对象的析构函数,然后再释放内存空间。在释放数组时,
free
函数不能自动区分数组中的元素类型,需要手动计算需要释放的内存大小。而delete[]
操作符会自动识别数组中每个元素的类型,并逐个调用其析构函数。delete
操作符只能用于释放通过new
操作符分配的内存,而free
可以用于释放任意由malloc
等函数分配的内存空间。当试图释放空指针时,
free
函数不会产生异常或错误,只是简单地忽略该操作;而delete
操作符会抛出一个std::bad_alloc
异常。
注意:对于使用 new
分配的内存,应该使用 delete
进行释放;对于使用 malloc
等函数分配的内存,应该使用 free
进行释放。在 C++ 中不应该混用 new
和 malloc
等函数分配的内存空间,否则可能会导致未定义的行为和内存泄漏等问题。
10. free一个数组时如何知道要释放多大的内存呢?
在C或C++中,要释放一个数组,需要知道该数组占用的内存大小。如果你使用了动态内存分配函数(如malloc或calloc),则可以使用free函数来释放相应的内存。
为了知道要释放多大的内存,可以使用sizeof运算符来计算数组占用的内存大小。例如,如果你有一个整数数组arr,可以使用以下代码来释放它所占用的内存:
int* arr = (int*) malloc(10 * sizeof(int)); // 分配10个整数空间
// 使用数组
free(arr); // 释放内存
在上面的代码中,malloc函数分配了10个整数空间,并返回指向这些空间的指针。由于每个整数占用4个字节的内存,因此数组占用40个字节的内存。然后,我们可以使用free函数来释放这40个字节的内存,如上述代码所示。
11. _stdcall和__cdecl的区别?
stdcall和cdecl都是C语言中的函数调用约定,用于规定如何在程序中传递参数、返回值以及如何清理堆栈等。两者的主要区别在于参数的传递顺序、堆栈的清理方式以及函数名的修饰方式。
__cdecl是默认的调用约定,其特点是参数从右向左依次入栈,由调用者来清理堆栈,函数名不会被修改。例如:
int __cdecl add(int a, int b)
{
return a + b;
}
而__stdcall则是一种标准的Win API调用约定,其特点是参数从右向左依次入栈,由被调用函数来清理堆栈,函数名会被自动加上一个下划线。例如:
int __stdcall add(int a, int b)
{
return a + b;
}
使用__stdcall时需要注意以下几点:
被调用函数必须显式地声明为__stdcall,否则默认采用__cdecl调用约定。
使用__stdcall时,由于被调用函数负责清理堆栈,因此调用者不能通过修改栈指针来改变函数的返回地址,否则可能导致程序崩溃或结果异常。
总之,在实际应用中,我们需要根据具体情况选择合适的函数调用约定。如果是编写标准Win API库函数,建议使用__stdcall调用约定。而对于普通函数,可以使用默认的__cdecl调用约定。
12. linux内部提供了那些调试宏?
在Linux内核中提供了一些常用的调试宏,这些宏可以在代码中插入调试信息,帮助我们快速定位问题。其中比较常用的调试宏包括:
- printk:用于向控制台输出调试信息,类似于C语言中的printf函数。该宏用法如下:
printk(KERN_DEBUG "debug message\n");
- BUG_ON和WARN_ON:用于检测程序中的错误,并输出相应的调试信息。当检测到错误时,会触发内核崩溃或输出警告信息。这两个宏的用法如下:
BUG_ON(condition); // 检测condition是否为真,如果为真,则触发内核崩溃
WARN_ON(condition); // 检测condition是否为真,如果为真,则输出警告信息
- dump_stack:用于输出当前堆栈信息,帮助我们分析程序运行状态。该宏用法如下:
dump_stack();
- trace_printk:用于记录程序执行过程中的跟踪信息,与printk类似,但可以在性能开销较小的情况下进行跟踪。该宏用法如下:
trace_printk("trace message\n");
除了以上几个调试宏外,还有许多其他的调试宏可供使用,例如dev_dbg、netdev_dbg、audit等。这些宏可以根据具体的应用场景选择使用。
13. 手写线程安全的单例模式?
/////////////////// 加锁的懒汉式实现 //////////////////
class SingleInstance
{
public:
// 获取单实例对象
static SingleInstance *&GetInstance();
//释放单实例,进程退出时调用
static void deleteInstance();
// 打印实例地址
void Print();
private:
// 将其构造和析构成为私有的, 禁止外部构造和析构
SingleInstance();
~SingleInstance();
// 将其拷贝构造和赋值构造成为私有函数, 禁止外部拷贝和赋值
SingleInstance(const SingleInstance &signal);
SingleInstance &operator=(const SingleInstance &signal);
private:
// 唯一单实例对象指针
static SingleInstance *m_SingleInstance;
static std::mutex m_Mutex;
};
//初始化静态成员变量
SingleInstance *SingleInstance::m_SingleInstance = NULL;
std::mutex SingleInstance::m_Mutex;
SingleInstance *&SingleInstance::GetInstance()
{
// 这里使用了两个 if判断语句的技术称为双检锁;好处是,只有判断指针为空的时候才加锁,
// 避免每次调用 GetInstance的方法都加锁,锁的开销毕竟还是有点大的。
if (m_SingleInstance == NULL)
{
std::unique_lock lock(m_Mutex); // 加锁
if (m_SingleInstance == NULL)
{
m_SingleInstance = new (std::nothrow) SingleInstance;
}
}
return m_SingleInstance;
}
void SingleInstance::deleteInstance()
{
std::unique_lock lock(m_Mutex); // 加锁
if (m_SingleInstance)
{
delete m_SingleInstance;
m_SingleInstance = NULL;
}
}
void SingleInstance::Print()
{
std::cout "我的实例内存地址是:" this }
SingleInstance::SingleInstance()
{
std::cout "构造函数" }
SingleInstance::~SingleInstance()
{
std::cout "析构函数" }
/////////////////// 加锁的懒汉式实现 //////////////////
在c++11后引入了线程安全的实现方式,推荐使用这种方式:
/////////////////// 内部静态变量的懒汉实现 //////////////////
class Single
{
public:
// 获取单实例对象
static Single &GetInstance();
// 打印实例地址
void Print();
private:
// 禁止外部构造
Single();
// 禁止外部析构
~Single();
// 禁止外部复制构造
Single(const Single &signal);
// 禁止外部赋值操作
Single &operator=(const Single &signal);
};
Single &Single::GetInstance()
{
// 局部静态特性的方式实现单实例
static Single signal;
return signal;
}
void Single::Print()
{
std::cout "我的实例内存地址是:" this }
Single::Single()
{
std::cout "构造函数" }
Single::~Single()
{
std::cout "析构函数" }
/////////////////// 内部静态变量的懒汉实现 //////////////////
饿汉模式天生线程安全:
////////////////////////// 饿汉实现 /////////////////////
class Singleton
{
public:
// 获取单实例
static Singleton* GetInstance();
// 释放单实例,进程退出时调用
static void deleteInstance();
// 打印实例地址
void Print();
private:
// 将其构造和析构成为私有的, 禁止外部构造和析构
Singleton();
~Singleton();
// 将其拷贝构造和赋值构造成为私有函数, 禁止外部拷贝和赋值
Singleton(const Singleton &signal);
Singleton &operator=(const Singleton &signal);
private:
// 唯一单实例对象指针
static Singleton *g_pSingleton;
};
// 代码一运行就初始化创建实例 ,本身就线程安全
Singleton* Singleton::g_pSingleton = new (std::nothrow) Singleton;
Singleton* Singleton::GetInstance()
{
return g_pSingleton;
}
void Singleton::deleteInstance()
{
if (g_pSingleton)
{
delete g_pSingleton;
g_pSingleton = NULL;
}
}
void Singleton::Print()
{
std::cout "我的实例内存地址是:" this }
Singleton::Singleton()
{
std::cout "构造函数" }
Singleton::~Singleton()
{
std::cout "析构函数" }
////////////////////////// 饿汉实现 /////////////////////
特点与选择
懒汉式是以时间换空间,适应于访问量较小时;推荐使用内部静态变量的懒汉单例,代码量少。
饿汉式是以空间换时间,适应于访问量较大时,或者线程比较多的的情况。
14. 引用和指针的区别?
C++ 中,指针和引用都是用来操作内存地址的。它们之间的主要区别如下:
指针可以被重新赋值和指向新的对象,而引用一旦绑定到一个对象上就不能再绑定到其他的对象上。
指针可以为 NULL,表示不指向任何对象,而引用必须始终绑定到某个对象上。
指针需要通过解引用(*)符号来访问所指向的对象,而引用直接访问所绑定的对象。
指针可以进行算术运算(加减),而引用不支持算术运算。
在函数参数列表中,当使用指针传递参数时,函数可以修改指针所指向的对象;当使用引用传递参数时,函数可以直接修改引用所绑定的对象。
总的来说,引用更易于使用和理解,因为它们提供了更简洁的语法,而且不会发生空指针的问题。但是在某些情况下,指针仍然是必需的,例如在动态内存分配、函数返回多个值时等。