c++面试题

1.linux下线程相关

线程的概念:

  • 进程是操作系统中资源管理的最小单位。线程是程序执行的最小单位。
  • 在操作系统设计上,从进程演化出线程最主要的目的就是更好地支持多处理器以及减少上下文切换开销。
  • 一个进程至少需要一个线程作为它的指令执行体,进程管理着计算机资源,而将线程分配到某个CPU上执行。
  • 对操作系统来说,进程占有系统资源,进程的切换也给操作系统带来了额外的开销。每次创建新进程会把父进程的资源复制一份到子进程,如果创建多个进程的话,会占用大量的资源。
  • 进程间的数据共享也需要操作系统的干预。
  • 线程是一种轻量级的进程。
  • 线程没有系统资源。
  • 线程是操作系统调度的最小单位,一个进程由一个或多个线程组成。
  • 在操作系统内核中,是按照线程作为调度单位来调度资源的。
  • 在一个进程内部,多个线程之间的资源是共享的。
  • 目前Linux中最流行的线程机制为LinuxThreads,采用的是基于核心轻量级进程的“一对一”模型。一个线程对应一个核心轻量级进程,进程调度由Linux内核完成,而线程的管理在核外函数库中实现。

进程与线程的对比:

进程和线程有许多相似之处,但是也有许多不同:

  1. 资源分配不同
    进程拥有独立的内存和系统资源,而在一个进程内部,线程之间的资源是共享的,系统不会为线程分配系统资源。

  2. 工作效率不同
    进程拥有系统资源,在系统切换的时候,操作系统要保留进程占用的资源;线程的切换不需要保留系统资源,切换效率远高于进程。

  3. 执行方式不同
    线程有程序运行的入口地址,但是线程不能独立运行。由于线程不占有系统资源,所以线程必须放在进程中。进程可以被操作系统直接调度。一个进程内部的多个线程可共享资源和调度,不同进程之间的线程资源不能直接共享。

系统调用线程控制的过程:

  1. 线程创建
    格式:
#include <pthread.h>
    int pthread_create(pthread_t *thread, constpthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

说明:pthread_create()为调用的进程创建一个新线程。其中参数thread为线程标识符、attr为线程属性设置、start_routine为线程函数起始地址、arg为传递给start_routine的参数。
创建线程成功时,返回0,创建线程失败,返回错误号。pthread_create是通过系统调用clone来实现的,clone是Linux特有的系统调用,类似进程创建的系统调用fork()

  1. 获得线程标识符
    格式:
    #include <pthread.h>
    pthread_tpthread_self(void);

说明:pthread_tpthread_self()返回调用的线程的标识符。每个线程都有自己的线程标识符,以便在进程内区分,线程标识符在pthread_create创建时产生。

  1. 线程等待
    格式:
   #include <pthread.h>
   int pthread_join(pthread_t thread, void **retval);

说明:pthread_join()将调用它的线程阻塞,一直等到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。第一个参数thread为被等待的线程标识符,第二个参数retval为用户定义的指针,存放被等待线程的返回值。

  1. 线程退出
    格式:
   #include <pthread.h>
   void pthread_exit(void *retval);
   int pthread_cancel(pthread_t thread);

说明:pthread_exit()终止调用线程,retval为线程的返回值;pthread_cancel终止由参数thread指定的线程。

程序要求:编写程序创建一个线程,该线程显示3次字符串This is apthread,父进程显示3次字符串This is the main process

#include <stdio.h>
#include <pthread.h>
void thread(void)
{
   int i;
   for (i=0;i<3;i++)
      printf("This is a pthread.\n");
}

int main(void)
{
   pthread_t id;
   int i,ret;
   ret=pthread_create(&id,NULL,(void *) thread,NULL);
   if (ret!=0)
   {
       printf ("Create pthread error!\n");
       exit (1);
   }
   for (i=0;i<3;i++)
       printf("This is the main process.\n");
   pthread_join(id,NULL);
   return(0);
}

编译并运行,因线程相关函数是运行在用户空间的线程库pthread.h实现,所以编译的时候要加上-lpthread选项。

linux下进程间通信的几种主要手段简介:

  1. 管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;

  2. 信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数);

  3. 报文(Message)队列(消息队列):消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。

  4. 共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。

  5. 信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。

  6. 套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。

一般来说,linux下的进程包含以下几个关键要素:

  • 有一段可执行程序;
  • 有专用的系统堆栈空间;
  • 内核中有它的控制块(进程控制块),描述进程所占用的资源,这样,进程才能接受内核的调度;
  • 具有独立的存储空间

进程间的五种通信方式介绍 - wh_sjc的博客 - CSDN博客

线程间的通信、同步方式与进程间通信方式 - 星仔的“韵” - CSDN博客

线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。

Linux系统中的线程间通信方式主要以下几种:

  1. 锁机制:包括互斥锁、条件变量、读写锁和自旋锁。
  • 互斥锁:确保同一时间只能有一个线程访问共享资源。当锁被占用时试图对其加锁的线程都进入阻塞状态(释放CPU资源使其由运行状态进入等待状态)。当锁释放时哪个等待线程能获得该锁取决于内核的调度。

  • 读写锁:当以写模式加锁而处于写状态时任何试图加锁的线程(不论是读或写)都阻塞,当以读状态模式加锁而处于读状态时“读”线程不阻塞,“写”线程阻塞。读模式共享,写模式互斥。

  • 条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。

  • 自旋锁上锁受阻时线程不阻塞而是在循环中轮询查看能否获得该锁,没有线程的切换因而没有切换开销,不过对CPU的霸占会导致CPU资源的浪费。 所以自旋锁适用于并行结构(多个处理器)或者适用于锁被持有时间短而不希望在线程切换产生开销的情况。

  1. 信号量机制(Semaphore):包括无名线程信号量和命名线程信号量

  2. 信号机制(Signal):类似进程间的信号处理

linux基础——linux线程间通信及同步机制总结 - 千里之行,始于足下 - CSDN博客

2.STL常见方法

成员函数说明
begin()返回指向容器中第一个元素的迭代器。
end()返回指向容器中最后一个元素后面的位置的迭代器。
rbegin()返回指向容器中最后一个元素的反向迭代器。
rend()返回指向容器中第一个元素前面的位置的反向迭代器。
erase(…)从容器中删除一个或几个元素。该函数参数较复杂,此处省略。
clear()从容器中删除所有元素。

3.c++内存模型

C分为四个区:堆,栈,静态全局变量区,常量区

C++内存分为5个区域(堆栈全常代 ):

  1. 堆 heap :
    由new分配的内存块,其释放编译器不去管,由我们程序自己控制(一个new对应一个delete)。如果程序员没有释放掉,在程序结束时OS会自动回收。涉及的问题:“缓冲区溢出”、“内存泄露”

  2. 栈 stack :
    是那些编译器在需要时分配,在不需要时自动清除的存储区。存放局部变量、函数参数。
    存放在栈中的数据只在当前函数及下一层函数中有效,一旦函数返回了,这些数据也就自动释放了。

  3. 全局/静态存储区 (.bss段和.data段) :
    全局和静态变量被分配到同一块内存中。在C语言中,未初始化的放在.bss段中,初始化的放在.data段中;在C++里则不区分了。

  4. 常量存储区 (.rodata段) :
    存放常量,不允许修改(通过非正当手段也可以修改)

  5. 代码区 (.text段) :
    存放代码(如函数),不允许修改(类似常量存储区),但可以执行(不同于常量存储区)

根据c/c++对象生命周期不同,c/c++的内存模型有三种不同的内存区域,即

  • 自由存储区,动态区、静态区。
  • 自由存储区:局部非静态变量的存储区域,即平常所说的栈
  • 动态区: 用operator new ,malloc分配的内存,即平常所说的堆
  • 静态区:全局变量 静态变量 字符串常量存在位置
    而代码虽然占内存,但不属于c/c++内存模型的一部分

在linux系统中,程序在内存中的分布如下所示:

低地址
.text—> .data —>.bss

—>heap(堆) —> unused <— stack(栈)

–>env
高地址

其中 :

  • .text 部分是编译后程序的主体,也就是程序的机器指令。

  • .data 和 .bss 保存了程序的全局变量,.data保存有初始化的全局变量,.bss保存只有声明没有初始化的全局变量。

  • heap(堆)中保存程序中动态分配的内存,比如C的malloc申请的内存,或者C++中new申请的内存。堆向高地址方向增长。

  • stack(栈)用来进行函数调用,保存函数参数,临时变量,返回地址等。

BSS 是“Block Started by Symbol”的缩写,意为“以符号开始的块”。BSS是Unix链接器产生的未初始化数据段。其他的段分别是包含程序代码的 “text”段和包含已初始化数据的“data”段。BSS段的变量只有名称和大小却没有值。此名后来被许多文件格式使用,包括PE。“以符号开始的块”指的是编译器处理未初始化数据的地方。BSS节不包含任何数据,只是简单的维护开始和结束的地址,以便内存区能在运行时被有效地清零。BSS节在应用程序的二进制映象文件中并不存在。

在采用段式内存管理的架构中(比如intel的80x86系统),bss段(Block Started by Symbol segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域,一般在初始化时bss 段部分将会清零。bss段属于静态内存分配,即程序一开始就将其清零了。

比如,在C语言之类的程序编译完成之后,已初始化的全局变量保存在.data 段中,未初始化的全局变量保存在.bss 段中。

text和data段都在可执行文件中(在嵌入式系统里一般是固化在镜像文件中),由系 统从可执行文件中加载;而bss段不在可执行文件中,由系统初始化。

各个段的关系
一个正在运行着的C编译程序占用的内存分为代码区、初始化数据区、未初始化数据区、堆区 和栈区5个部分。

  1. 代码区(text segment)。代码区指令根据程序设计流程依次执行,对于顺序指令,则只会执行一次(每个进程),如果反复,则需要使用跳转指令,如果进行递归,则需 要借助栈来实现。
    代码区的指令中包括操作码和要操作的对象(或对象地址引用)。如果是立即数(即具体的数值,如5),将直接包含在代码中;如果是局部数据,将在栈区 分配空间,然后引用该数据地址;如果是BSS区和数据区,在代码中同样将引用该数据地址。

  2. 全局初始化数据区/静态数据区(Data Segment)。只初始化一次。

  3. 未初始化数据区(BSS)。在运行时改变其值。

  4. 栈区(stack)。由编译器自动分配释放,存放函数的参数值、局部变量的值等。其操作方式类似于数据结构中的栈。每当一个函数被调用,该函 数返回地址和一些关于调用的信息,比如某些寄存器的内容,被存储到栈区。然后这个被调用的函数再为它的自动变量和临时变量在栈区上分配空间,这就是C实现 函数递归调用的方法。每执行一次递归函数调用,一个新的栈框架就会被使用,这样这个新实例栈里的变量就不会和该函数的另一个实例栈里面的变量混淆。

  5. 堆区(heap)。用于动态内存分配。堆在内存中位于bss区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时有可能由OS 回收。

之所以分成这么多个区域,主要基于以下考虑:

一个进程在运行过程中,代码是根据流程依次执行的,只需要访问一次,当然跳转和递归有可能使代码执行多次,而数据一般都需要访问多次,因此单独开辟 空间以方便访问和节约空间。
临时数据及需要再次使用的代码在运行时放入栈区中,生命周期短。
全局数据和静态数据有可能在整个程序执行过程中都需要访问,因此单独存储管理。
堆区由用户自由分配,以便管理。

下面通过一段简单的代码来查看C程序执行时的内存分配情况。相关数据在运行时的位置如注释所述。

//main.cpp
int a = 0;        //a在全局已初始化数据区
char *p1;        //p1在BSS区(未初始化全局变量)
main()
{
	int b;        //b在栈区
	char s[] = "abc";  //s为数组变量,存储在栈区,
	//"abc"为字符串常量,存储在已初始化数据区
	char *p1,*p2;    //p1、p2在栈区
	char *p3 = "123456";  //123456\0在已初始化数据区,p3在栈区
	static int c =0;    //C为全局(静态)数据,存在于已初始化数据区
	//另外,静态数据会自动初始化
	p1 = (char *)malloc(10);//分配得来的10个字节的区域在堆区
	p2 = (char *)malloc(20);//分配得来的20个字节的区域在堆区
	free(p1);
	free(p2);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值