参考自xd-ruisi的一个帖子
c语言:
内容部分学习自:http://c.biancheng.net/cpp/biancheng/view/238.html 非常好用的网站呀 良心推荐 清晰易懂
Q:程序和数据存放内存分配
Q:下列各个程序所占内存大小
//由""包括的字符串会自动在末尾添加 /0
//逐个字符赋值并不会添加/0
char s1[] = { "dadsa" };//末尾有/0 所以是6
char s2[][80] = { "c++","jaca","c","asdads" };//4乘以8
char s3[] = "adsad";//6个
char *s4 = "absda";//
cout << "s1:" << sizeof(s1) << endl;
cout << "s2:" << sizeof(s2) << endl;
cout << "s3:" << sizeof(s3) << endl;
cout << "s4:" << sizeof(s4) << endl;
Q:关于字符串数组概念
http://c.biancheng.net/cpp/html/93.html
例题:
struct {
int x;
int y;
}num[2] = { 1,2,3,4 };
挨个赋值:num[0].x = 1,num[0].y=2;num[1].x = 3,num[1].y=4;
Q:C语言结构体占用内存大小
https://www.nowcoder.com/questionTerminal/e364954b58db4929a9db8a6845380c74?toCommentId=124956
例子:
struct stu{
uint16_t A;//2
uint32_t B;//4
uint8_t C;//1
}stu1;
cout << "sizeof(stu1):" << sizeof(stu1) << endl;
//答案是12
Q: c++里是怎么定义常量的?常量存放在内存的哪个位置?
BSS段 :通常是指用来存放程序中 未初始化的全局变量、静态变量(全局变量未初始化时默认为0)的一块内存区域
数据段 :通常是指用来存放程序中 初始化后的全局变量和静态变量
代码段 :通常是指用来存放程序中 代码和常量
堆 :通常是指用来存放程序中 进程运行时被动态分配的内存段 ( 动态分配:malloc / new,者动态释放:free / delete)
栈 :通常是指用来存放程序中 用户临时创建的局部变量、函数形参、数组(局部变量未初始化则默认为垃圾值)也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除 以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进后出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。它是由操作系统分配的,内存的申请与回收都由OS管理。
Q:new和delete操作符
c语言中分配内存使用malloc函数,释放内存使用free()函数
int *p=(int*) malloc(sizeof(int)*10);
free(p)
c++使用new和delete来分配一组连续的数据,可以使用new[]
int *p = new int[10];
delete[] p;
Q:inline函数
函数调用是有时间和空间开销的,为了消除函数调用的时间开销,c++提供了一种提高效率的办法,即在编译时,函数调用处用函数体替换,类似于c语言中的宏展开。这种在函数调用处直接嵌入函数体的函数称为内联函数inline function,一般只将那些短小的、频繁调用的函数声明为内联函数。
Q:关于const
顶层const:指针本身是常亮,不能指向其他地方 int *const p = &a;
底层const:指针指向的对象是一个常量。 int const *p = &a;
关于修饰成员函数:
对数据成员:数据只读,不能修改
成员函数:在类中将成员函数修饰为const表明在该函数体内,不能修改对象的数据成员而且不能调用非const函数。为什么不能调用非const函数?因为非const函数可能修改数据成员,const成员函数是不能修改数据成员的,所以在const成员函数内只能调用const函数。
如果同时定义了两个函数,一个带const,一个不带,相当于函数的重载
如果在const修饰成成员函数中还是想改变成员变量怎么办?mutable
如果类的成员函数不会改变对象的状态,那么这个成员函数一般会声明成const的。但是,有些时候,我们需要在const的函数里面修改一些跟类状态无关的数据成员,那么这个数据成员就应该被mutalbe来修饰
Q:extern C
C++调用C函数需要extern C,因为C语言没有函数重载。实际上就是取消了name mangling。让编译器能够在库中找到正确的符号(或者收函数签名)。
解释下:就是C的库是按照C编译器编译的。所以其是没有C++的name mangling(即C++编译器带来的)。如果通过C++编译器的话, 头文件中函数的签名就会被name mangling,就会加一些多余的东西。这样的话,就相当于和C库中的符号不对应了。
#ifdef __cplusplus
extern "C"{
#endif
#include <c语言.h>
#ifdef __cplusplus
}
#endif
Q:malloc/free与new/delete
差别:
1. malloc/free是标准库函数, new/delete 是表达式;
2. malloc只负责开辟空间,但并不会进行初始化;new表达式开辟空间之后,也会构造函数进行初始化;
3. malloc 返回的指针类型是 void* ,而 new 返回的指针是有类型的
4. delete会调用析构函数。
void* (可以指向任意类型的指针,也就是说可以用任意类型的指针对void指针对void指针赋值)
Q:引用和指针的概念
相同点: 都有地址的概念
不同点:
1. 指针是可以独立存在的,但是引用不行引用必须要进行初始化,指针没有必要
2. 指针可以设置为NULL, 但是引用不能有空NULL
3. 引用一旦进行初始化之后,不会再改变其指向,但指针可以
Java中的引用概念和C++不太一样。有点类似C++中的指针。
Q:四种强制类型转换
参考自:http://c.biancheng.net/view/410.html
- static_cast:static_cast<type-id> (expression)。
用法:将expression转换为type-id类型,但没有运行时类型检查保证转换时的安全性
#include <iostream>
using namespace std;
class A
{
public:
operator int() { return 1; }
operator char*() { return NULL; }
};
int main()
{
A a;
int n;
char* p = "New Dragon Inn";
n = static_cast <int> (3.14); // n 的值变为 3
n = static_cast <int> (a); //调用 a.operator int,n 的值变为 1
p = static_cast <char*> (a); //调用 a.operator char*,p 的值变为 NULL
n = static_cast <int> (p); //编译错误,static_cast不能将指针转换成整型
p = static_cast <char*> (n); //编译错误,static_cast 不能将整型转换成指针
return 0;
}
1.基类和派生类之间指针和引用的转换
上行转换(派生类指针或者引用转换为基类表示)安全
下行转换(把基类指针或引用转换成派生类表示)不安全(动态类型检查)
2.用于基本数据类型之间的转换
3.空指针转换为任意类型指针,任意类型指针转换为void
void *p= &d;//任何非常量对象的地址都可以存入void*
double *dp = static_cast<double*>(p);//将void*转回初始的指针类型
static_cast不能用于在不同指针之间,整型和指针,不同类型的引用之间的转换
- const_cast:const_cast <type-id> (expression)
将常量对象改变成非常量对象的行为,即去掉“const”性质,一旦去掉了const性质,编译器就不再组织我们对改对象进行写操作了
//const_cast只能改变对象的底层const:指针指向的对象是const
const char *pc;
char *p = const_cast<char*>pc;
reinterpret_cast: <type-id> (expression)
概念:用于不同类型的指针,引用,指针和能容纳指针的整数类型之间的转换,转换时,执行的是逐个比特复制的操作
该运算符把exdivssion转换成type-id类型的对象。Type-id必须是类的指针、类的引用或者void *;
如果type-id是类指针类型,那么exdivssion也必须是一个指针,如果type-id是一个引用,那么exdivssion也必须是一个引用。
dynamic_cast:<type-id> (e) 运行时类型检查
用法:用于将基类的指针或者引用安全的转换为派生类的指针或者引用
- 1.其他三种都是编译时完成的,dynamic_cast是运行时处理的
- 2.不能用于内置类型的强制类型转换
- 3.进行上行转换的时候,dynamic_cast和static_cast的效果是一样的。在进行下行转换时候,dynamic_cast具有类型检查的功能,比static_cast不安全
- 4.使用dynamic_cast进行转换时,基函数一定要有虚函数,否则编译不通过
转换必须满足的条件:
1.e的类型必须是目标类型type的公有派生类
2.e的类型必须是目标类型的公有基类或者e的类型就是目标type的类型
Q:RTTI
运行时类型识别
dynamic_cast和type_id
1. 如果表达式的类型是类类型且至少包含有一个虚函数,则typeid操作符返回表达式的
动态类型,需要在运行时计算;
2. 否则,typeid操作符返回表达式的静态类型,在编译时就可以计算。
Q:类和对象
- 在类内部定义一个成员函数默认是内联函数
构造函数:
- 构造函数是允许重载的,创建对象时根据传递的实参来判断调用的是哪一个构造函数
- 如果用户没有定义构造函数,编译器会自动生成一个默认的构造函数
- 调用没有参数的构造函数也可以省略括号
对于示例2的代码,在栈上创建对象可以写作Student stu()
或Student stu
,在堆上创建对象可以写作Student *pstu = new Student()
或Student *pstu = new Student
,它们都会调用构造函数 Student()。
- 构造函数初始化顺序和初始化列表中列出的变量顺序无关,他只与成员变量在类中生命的顺序有关
- 初始化const成员变量的唯一方法是使用初始化列表
class VLA{
private:
const int m_len;
int *m_arr;
public:
VLA(int len);
};
//必须使用初始化列表来初始化 m_len
VLA::VLA(int len): m_len(len){
m_arr = new int[len];
}
析构函数:
析构函数的执行时机:
在所有函数之外创建的对象是全局对象,它和全局变量类似,位于内存分区中的全局数据区,程序在结束执行时会调用这些对象的析构函数。
在函数内部创建的对象是局部对象,它和局部变量类似,位于栈区,函数执行结束时会调用这些对象的析构函数。
new 创建的对象位于堆区,通过 delete 删除时才会调用析构函数;如果没有 delete,析构函数就不会被执行。
Q:this指针:
this是一个关键字,也是一个const指针,他指向当前对象(当前正在访问的对象),通过他可以访问当前对象的所有成员
this 实际上是成员函数的一个形参,在调用成员函数时将对象的地址作为实参传递给 this。不过 this 这个形参是隐式的,它并不出现在代码中,而是在编译阶段由编译器默默地将它添加到参数列表中。
this 作为隐式形参,本质上是成员函数的局部变量,所以只能用在成员函数的内部,并且只有在通过对象调用成员函数时才给 this 赋值。
只有当对象被创建后this才有意义,因此不能砸static成员函数中使用。
#include <iostream>
using namespace std;
class Student{
public:
void setname(char *name);
void setage(int age);
void setscore(float score);
void show();
private:
char *name;
int age;
float score;
};
void Student::setname(char *name){
this->name = name;
}
void Student::setage(int age){
this->age = age;
}
void Student::setscore(float score){
this->score = score;
}
void Student::show(){
cout<<this->name<<"的年龄是"<<this->age<<",成绩是"<<this->score<<endl;
}
int main(){
Student *pstu = new Student;
pstu -> setname("李华");
pstu -> setage(16);
pstu -> setscore(96.5);
pstu -> show();
return 0;
}
Q:stastic静态成员变量:
https://www.cnblogs.com/Liu269393/p/10943109.html
class Student{
public:
Student(char *name, int age, float score);
void show();
public:
static int m_total; //静态成员变量
private:
char *m_name;
int m_age;
float m_score;
};
静态成员变量属于类但是不属于具体的某个对象
1.为什么stastic只能在类外初始化
因为静态成员属于整个类,而不属于某个对象,如果在类内初始化,在每次建立多个对象的时候会多次声明和定义该变量的存储位置,在名字空间和作用相同的情况下会导致重名问题
对象的内存包含了成员变量,不同的对象会占用不同的内存,这使得不同对象成员变量相互独立,他们的值不受其他对象的影响,例如有两个相同类型的对象a,b,他们都有一个成员变量name,a.name和b.name是不同的
但是我们希望有时候希望在多个对象之间共享数据,对象 a 改变了某份数据后对象 b 可以检测到。共享数据的典型使用场景是计数,以前面的 Student 类为例,如果我们想知道班级中共有多少名学生,就可以设置一份共享的变量,每次创建对象时让该变量加 1。
我们可以使用静态成员变量来实现多个对象共享数据的目标。静态成员变量是一种特殊的成员变量,它被关键字static
修饰
class Student{
public:
Student(char *name, int age, float score);
void show();
public:
static int m_total; //静态成员变量
private:
char *m_name;
int m_age;
float m_score;
};
static对象只能在类外初始化,具体形式为:int Student::m_total = 0;
注意:static成员变量不占用对象的内存,而是在所有的对象之外开辟,即使不创建对象也可以访问。具体来说,static成员变量和普通的static成员变量相似,都在内存分区的全局数据区分配内存
#include <iostream>
using namespace std;
class Student{
public:
Student(char *name, int age, float score);
void show();
private:
static int m_total; //静态成员变量
private:
char *m_name;
int m_age;
float m_score;
};
//初始化静态成员变量
int Student::m_total = 0;
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){
m_total++; //操作静态成员变量
}
void Student::show(){
cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<"(当前共有"<<m_total<<"名学生)"<<endl;
}
int main(){
//创建匿名对象
(new Student("小明", 15, 90)) -> show();
(new Student("李磊", 16, 80)) -> show();
(new Student("张华", 16, 99)) -> show();
(new Student("王康", 14, 60)) -> show();
return 0;
}
注意几点:
- 一个类中可以有一个或者多个静态成员变量
- static成员变量和普通的static成员变量一样,都在内存分区的全局数据区域分配内存,到程序结束时才释放,这就意味着,static 成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。而普通成员变量在对象创建时分配内存,在对象销毁时释放内存。
- static的初始化只能在类外进行
- 静态成员变量既可以通过对象名访问,也可以通过类名访问,但要遵循 private、protected 和 public 关键字的访问权限限制。当通过对象名访问时,对于不同的对象,访问的是同一份内存。
Q:静态成员函数
普通成员函数可以访问所有成员变量,静态成员函数可以访问静态成员。
编译器在编译一个普通的成员函数的时候,会隐式的增加一个形参this,并把当前对象的地址赋值给this,所以普通成员函数只能在创建对象后使用,因为他需要当前对象的地址。
而静态成员函数可以通过类直接调用
普通成员变量占用对象的内存,静态成员函数没有 this 指针,不知道指向哪个对象,无法访问对象的成员变量,也就是说静态成员函数不能访问普通成员变量,只能访问静态成员变量
静态成员函数与普通成员函数的根本区别在于:普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)。
#include <iostream>
using namespace std;
class Student{
public:
Student(char *name, int age, float score);
void show();
public: //声明静态成员函数
static int getTotal();
static float getPoints();
private:
static int m_total; //总人数
static float m_points; //总成绩
private:
char *m_name;
int m_age;
float m_score;
};
int Student::m_total = 0;
float Student::m_points = 0.0;
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){
m_total++;
m_points += score;
}
void Student::show(){
cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl;
}
//定义静态成员函数
int Student::getTotal(){
return m_total;
}
float Student::getPoints(){
return m_points;
}
int main(){
(new Student("小明", 15, 90.6)) -> show();
(new Student("李磊", 16, 80.5)) -> show();
(new Student("张华", 16, 99.0)) -> show();
(new Student("王康", 14, 60.8)) -> show();
int total = Student::getTotal();
float points = Student::getPoints();
cout<<"当前共有"<<total<<"名学生,总成绩是"<<points<<",平均分是"<<points/total<<endl;
return 0;
}
Q:c++类与const关键字
const可以修饰成员变量、成员函数以及对象
1.const成员变量的用法和普通的相似
2.const成员函数的用法:
//声明常成员函数
char *getname() const;
int getage() const;
float getscore() const;
const成员函数仅仅是为了获取成员变量的值,没有修改任何成员变量的企图
3.const对象
const也可以用来修饰const对象,成为常对象,就只能调用类的const成员了
const class object(params);
class const object(params);
const class *p = new class(params);
class const *p = new class(params);
#include <iostream>
using namespace std;
class Student{
public:
Student(char *name, int age, float score);
public:
void show();
char *getname() const;
int getage() const;
float getscore() const;
private:
char *m_name;
int m_age;
float m_score;
};
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){ }
void Student::show(){
cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl;
}
char * Student::getname() const{
return m_name;
}
int Student::getage() const{
return m_age;
}
float Student::getscore() const{
return m_score;
}
int main(){
const Student stu("小明", 15, 90.6);
//stu.show(); //error
cout<<stu.getname()<<"的年龄是"<<stu.getage()<<",成绩是"<<stu.getscore()<<endl;
const Student *pstu = new Student("李磊", 16, 80.5);
//pstu -> show(); //error
cout<<pstu->getname()<<"的年龄是"<<pstu->getage()<<",成绩是"<<pstu->getscore()<<endl;
return 0;
}
Q:friend友元函数和友元类
Q:class和struct的区别
在C语言中,struct 只能包含成员变量,不能包含成员函数。而在C++中,struct 类似于 class,既可以包含成员变量,又可以包含成员函数。
struct和class的区别:
c++中的struct和class基本是通用的,只有几个细节不同
- class成员默认都是private的;而使用struct时,结构体中的成员默认都是public的
- class默认是private继承,struct默认是public继承
- class可以使用模板,struct不能
Q:c++面向对象
封装可以使代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用;
多态的目的是为了接口重用,最常见的用法就是声明基类类型的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法。如果没有使用虚函数的话,即没有利用C++多态性,则利用基类指针调用相应的函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数。因为没有多态性,函数调用的地址将是固定的,因此将始终调用到同一个函数,这就无法实现“一个接口,多种方法”的目的了。
- 静态多态:通常通过函数重载实现,函数调用的地址在编译器期间就可以确定函数的调用地址
- 动态多态:通常通过虚函数实现,虚函数允许派生类重新定义成员函数,而派生类重新定义基类的做法叫覆盖或者重写,函数调用的地址不能在编译器期间确定,必须在运行期间确定,属于晚绑定,也叫动态联编
继承:继承允许我们依据另一个类来定义一个类,达到了重用代码功能和提高执行效率的效果。
封装是把数据和函数绑定在一起的一个概念,这样能避免受到外界的干扰和误用,从而确保了安全。它指的是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。
数据封装是一种把数据和操作数据的函数捆绑在一起的机制,数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制
多态应用场景:
考虑这样一个场景:在 PPT 中要画很多形状,然后有一个 List 放了很多 形状(shape),但是有原形、椭圆形、三角形等形状要画,那么怎么一起管理这些形状呢?只要把这些不同的图形继承这个 形状 的基类,然后各自重写(覆盖) 基类中的 draw() 虚成员函数,这样虽然都是调用基类的指针,但是执行的是每个形状各自 draw() 方法。这就是多态的应用场景。
需要注意:
- 1.只有类的成员函数才能声明为虚函数,虚函数仅适用于有继承关系的类对象。普通函数不能声明为虚函数。
- 2.静态(static)成员函数不能是虚函数,因为静态成员函数不受限于某个对象。
- 3.内联函数(inline)不能是虚函数,因为内联函数不能在运行中动态确定位置。
- 4.构造函数不能是虚函数。
- 5.析构函数可以是虚函数,而且建议声明为虚函数。
Q:继承
参考自:http://c.biancheng.net/cpp/biancheng/view/2983.html
继承是类与类之间的关系,可以理解为一个类从另一个类获取成员变量和成员函数的过程。例如类A继承类B,那么B就拥有类A的成员变量和成员函数,被继承的类称为父类和基类,继承的类成为子类或派生类
典型的应用场景:
- 当你创建的新类与现有的类相似,只是多出几个成员变量和成员函数时,可以用继承,这样会减少代码量,而且新类会拥有基类的所有功能
#include<iostream>
using namespace std;
//基类 Pelple
class People {
public:
void setname(char *name);
void setage(int age);
char *getname();
int getage();
private:
char *m_name;
int m_age;
};
void People::setname(char *name) { m_name = name; }
void People::setage(int age) { m_age = age; }
char* People::getname() { return m_name; }
int People::getage() { return m_age; }
//派生类 Student
class Student : public People {
public:
void setscore(float score);
float getscore();
private:
float m_score;
};
void Student::setscore(float score) { m_score = score; }
float Student::getscore() { return m_score; }
int main() {
Student stu;
stu.setname("小明");
stu.setage(16);
stu.setscore(95.5f);
cout << stu.getname() << "的年龄是 " << stu.getage() << ",成绩是 " << stu.getscore() << endl;
return 0;
}
运行结果:
小明的年龄是 16,成绩是 95.5
本例中,People 是基类,Student 是派生类。Student 类继承了 People 类的成员,同时还新增了自己的成员变量 score 和成员函数 setscore()、getscore()。这些继承过来的成员,可以通过子类对象访问,就像自己的一样。
继承时的名字遮蔽:
如果派生类中的成员(包括成员变量和成员函数)和基类中的重名,那么会遮蔽从基类继承过来的成员,所谓遮蔽,就是在派生类使用该成员的时候,就是在派生类中使用改成员而不是从基类中继承过来的
一个例子:
#include<iostream>
using namespace std;
class People {
public:
void show();
protected:
char *m_name;
int m_age;
};
void People::show() {
cout << "people:" << m_age << " " << m_age << endl;
}
class Student:public People {
public:
Student(char *name, int age, float score);
void show();//屏蔽基类的show
private:
float m_score;
};
Student::Student(char *name, int age, float score) {
m_name = name;
m_age = age;
m_score = score;
}
void Student::show() {
cout <<"student:"<< m_name << "的年龄是" << m_age << ",成绩是" << m_score << endl;
}
int main() {
Student s("小明", 16, 90.5);
s.show();
s.People::show();
system("pause");
return 0;
}
在这有一个关于派生类的构造函数的初始化问题:派生类不能再初始化列表中直接初始化基类的成员
https://blog.csdn.net/libaineu2004/article/details/19565229?utm_source=blogkpcl13
基类成员和派生类成员的名字一样时会造成遮蔽,这句话对于成员变量很好理解,对于成员函数要引起注意,不管函数的参数如何,只要名字一样就会造成遮蔽。换句话说,基类成员函数和派生类成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数,不管它们的参数是否一样
继承时候的构造函数问题:
类的构造函数不能被继承,在设计派生类时,对继承过来的成员变量的初始化工作也要由派生类的构造函数完成,但是大部分基类都有 private 属性的成员变量,它们在派生类中无法访问,更不能使用派生类的构造函数来初始化。
这种矛盾在C++继承中是普遍存在的,解决这个问题的思路是:在派生类的构造函数中调用基类的构造函数
#include<iostream>
using namespace std;
//基类People
class People {
protected:
char *m_name;
int m_age;
public:
People(char*, int);
};
People::People(char *name, int age) : m_name(name), m_age(age) {}
//派生类Student
class Student : public People {
private:
float m_score;
public:
Student(char *name, int age, float score);
void display();
};
//People(name, age)就是调用基类的构造函数
Student::Student(char *name, int age, float score) : People(name, age), m_score(score) { }
void Student::display() {
cout << m_name << "的年龄是" << m_age << ",成绩是" << m_score << "。" << endl;
}
int main() {
Student stu("小明", 16, 90.5);
stu.display();
return 0;
}
继承时候的析构函数问题:
与构造函数不同的是,在派生类的析构函数中不用显式地调用基类的析构函数,因为每个类只有一个析构函数,编译器知道如何选择,无需程序员干涉。
另外析构函数的执行顺序和构造函数的执行顺序也刚好相反:
- 创建派生类对象时,构造函数的执行顺序和继承顺序相同,即先执行基类构造函数,再执行派生类构造函数。
- 而销毁派生类对象时,析构函数的执行顺序和继承顺序相反,即先执行派生类析构函数,再执行基类析构函数。
Q:多继承
多继承的语法:
class D: public A, private B, protected C{
//类D新增加的成员
}
多继承下构造函数的调用顺序和他们在派生类中出现的顺序无关,而是和声明派生类时基类出现的顺序相同,
例如:即使写作
D(形参列表): B(实参列表), C(实参列表), A(实参列表){
//其他操作
}
那么也是先调用 A 类的构造函数,再调用 B 类构造函数,最后调用 C 类构造函数。
多继承的构造函数:
多继承下的构造函数和单继承下的形式基本相同,只是要在派生类的构造函数中调用多个基类的构造函数
#include <iostream>
using namespace std;
//基类
class BaseA{
public:
BaseA(int a, int b);
~BaseA();
protected:
int m_a;
int m_b;
};
BaseA::BaseA(int a, int b): m_a(a), m_b(b){
cout<<"BaseA constructor"<<endl;
}
BaseA::~BaseA(){
cout<<"BaseA destructor"<<endl;
}
//基类
class BaseB{
public:
BaseB(int c, int d);
~BaseB();
protected:
int m_c;
int m_d;
};
BaseB::BaseB(int c, int d): m_c(c), m_d(d){
cout<<"BaseB constructor"<<endl;
}
BaseB::~BaseB(){
cout<<"BaseB destructor"<<endl;
}
//派生类
class Derived: public BaseA, public BaseB{
public:
Derived(int a, int b, int c, int d, int e);
~Derived();
public:
void show();
private:
int m_e;
};
Derived::Derived(int a, int b, int c, int d, int e): BaseA(a, b), BaseB(c, d), m_e(e){
cout<<"Derived constructor"<<endl;
}
Derived::~Derived(){
cout<<"Derived destructor"<<endl;
}
void Derived::show(){
cout<<m_a<<", "<<m_b<<", "<<m_c<<", "<<m_d<<", "<<m_e<<endl;
}
int main(){
Derived obj(1, 2, 3, 4, 5);
obj.show();
return 0;
}
命名冲突:
当两个或多个基类中有同名的成员时,如果直接访问该成员,就会产生命名冲突,编译器不知道使用哪个基类的成员。这个时候需要在成员名字前面加上类名和域解析符::
,以显式地指明到底使用哪个类的成员,消除二义性。
#include <iostream>
using namespace std;
//基类
class BaseA{
public:
BaseA(int a, int b);
~BaseA();
public:
void show();
protected:
int m_a;
int m_b;
};
BaseA::BaseA(int a, int b): m_a(a), m_b(b){
cout<<"BaseA constructor"<<endl;
}
BaseA::~BaseA(){
cout<<"BaseA destructor"<<endl;
}
void BaseA::show(){
cout<<"m_a = "<<m_a<<endl;
cout<<"m_b = "<<m_b<<endl;
}
//基类
class BaseB{
public:
BaseB(int c, int d);
~BaseB();
void show();
protected:
int m_c;
int m_d;
};
BaseB::BaseB(int c, int d): m_c(c), m_d(d){
cout<<"BaseB constructor"<<endl;
}
BaseB::~BaseB(){
cout<<"BaseB destructor"<<endl;
}
void BaseB::show(){
cout<<"m_c = "<<m_c<<endl;
cout<<"m_d = "<<m_d<<endl;
}
//派生类
class Derived: public BaseA, public BaseB{
public:
Derived(int a, int b, int c, int d, int e);
~Derived();
public:
void display();
private:
int m_e;
};
Derived::Derived(int a, int b, int c, int d, int e): BaseA(a, b), BaseB(c, d), m_e(e){
cout<<"Derived constructor"<<endl;
}
Derived::~Derived(){
cout<<"Derived destructor"<<endl;
}
void Derived::display(){
BaseA::show(); //调用BaseA类的show()函数
BaseB::show(); //调用BaseB类的show()函数
cout<<"m_e = "<<m_e<<endl;
}
int main(){
Derived obj(1, 2, 3, 4, 5);
obj.display();
return 0;
}
Q:c++虚继承和虚基类
多继承是多个基类中继承来产生派生类的问题,多继承的派生类继承了所有父类的成员,但是不可避免的会出现错综复杂的设计问题
命名冲突就是不可避免的一个
为了解决多继承中的命名冲突和冗余数据的问题,c++提出了虚继承,使得派生类只保留一份间接基类的成员。
命名冲突举例:
//间接基类A
class A{
protected:
int m_a;
};
//直接基类B
class B: public A{
protected:
int m_b;
};
//直接基类C
class C: public A{
protected:
int m_c;
};
//派生类D
class D: public B, public C{
public:
void seta(int a){ m_a = a; } //命名冲突
void setb(int b){ m_b = b; } //正确
void setc(int c){ m_c = c; } //正确
void setd(int d){ m_d = d; } //正确
private:
int m_d;
};
int main(){
D d;
return 0;
}
//间接基类A
class A{
protected:
int m_a;
};
//直接基类B
class B: virtual public A{ //虚继承
protected:
int m_b;
};
//直接基类C
class C: virtual public A{ //虚继承
protected:
int m_c;
};
//派生类D
class D: public B, public C{
public:
void seta(int a){ m_a = a; } //正确
void setb(int b){ m_b = b; } //正确
void setc(int c){ m_c = c; } //正确
void setd(int d){ m_d = d; } //正确
private:
int m_d;
};
int main(){
D d;
return 0;
}
Q:向上转型
类其实也是一种数据类型,也可以发生数据类型转换,不过这种转换只有在基类和派生类之间才有意义,并且只能将派生类赋值给基类,包括将派生类对象赋值给基类对象、将派生类指针赋值给基类指针、将派生类引用赋值给基类引用,这在 C++ 中称为向上转型(Upcasting)。相应地,将基类赋值给派生类称为向下转型(Downcasting)。
#include <iostream>
using namespace std;
class A{
public:
A(int a);
public:
void display();
int m_a;
};
A::A(int a) :m_a(a) {}
void A::display() {
cout << "class A:" << m_a << endl;
}
class B:public A{
public:
B(int a, int b);
void display();
int m_b;
};
B::B(int a,int b):A(a),m_b(b){}
void B::display(){
cout << "class B:m_a " << m_a << " m_b " << m_b << endl;
}
int main() {
A a(10);
B b(66, 99);
//赋值前
a.display();
b.display();
cout << "--------------" << endl;
//赋值后
a = b;
a.display();
b.display();
system("pause");
return 0;
}
Q:多态
没有虚函数的时候,基类的指针指向了派生类的对象的时候,不能调用派生类的成员变量
为了消除这种尴尬,c++增加了虚函数virtual
#include <iostream>
using namespace std;
//基类People
class People {
public:
People(char *name, int age) :m_name(name), m_age(age) {};
virtual void display(); //声明为虚函数
protected:
char *m_name;
int m_age;
};
void People::display() {
cout <<"基类:"<< m_name << " " << m_age << endl;
}
//派生类Teacher
class Teacher : public People {
public:
Teacher(char *name, int age, int salary) :People(name, age), m_salary(salary) {}
virtual void display(); //声明为虚函数
private:
int m_salary;
};
void Teacher::display() {
cout << "派生类:"<<m_name << " " << m_age << " " << m_salary << "" << endl;
}
int main() {
People *p = new People("王志刚", 23);
p->display();
p = new Teacher("赵宏佳", 45, 8200);
p->display();
system("pause");
return 0;
}
基类可以按基类的方式来做事,也可以按照派生类的方式来做事,有多种表示方式,称为“多态”
借助引用也可以实现多态
int main(){
People p("王志刚", 23);
Teacher t("赵宏佳", 45, 8200);
People &rp = p;
People &rt = t;
rp.display();
rt.display();
return 0;
}
不过引用不像指针灵活,指针可以随时改变指向,而引用只能指代固定的对象,在多态性方面缺乏表现力,所以以后我们再谈及多态时一般是说指针
虚函数:虚函数对多态来说至关重要,有了虚函数才能实现多态
多态的必要条件:
- 必须存在继承关系
- 必须有同名的函数
- 存在基类的指针
- 纯虚函数和抽象类:
- 定义:纯属函数没有函数体,只有函数声明,在虚函数的结尾加上 =0,表名此函数是纯属函数
- 包含纯虚函数的类是抽象类,之所以说抽象是因为无法实例化,无法创建对象
#include <iostream>
using namespace std;
//线
class Line {
public:
Line(float len):m_len(len){}
virtual float area() = 0;
virtual float volume() = 0;
protected:
float m_len;
};
//矩形
class Rec : public Line {
public:
Rec(float len, float width): Line(len), m_width(width) { }
float area();
protected:
float m_width;
};
float Rec::area() { return m_len * m_width; }
//长方体
class Cuboid : public Rec {
public:
Cuboid(float len, float width, float height) : Rec(len, width), m_height(height) { }
float area();
float volume();
protected:
float m_height;
};
float Cuboid::area() { return 2 * (m_len*m_width + m_len*m_height + m_width*m_height); }
float Cuboid::volume() { return m_len * m_width * m_height; }
//正方体
class Cube : public Cuboid {
public:
Cube(float len) : Cuboid(len, len, len) { }
float area();
float volume();
};
float Cube::area() { return 6 * m_len * m_len; }
float Cube::volume() { return m_len * m_len * m_len; }
int main() {
Line *p = new Cuboid(10, 20, 30);
cout << "The area of Cuboid is " << p->area() << endl;
cout << "The volume of Cuboid is " << p->volume() << endl;
p = new Cube(15);
cout << "The area of Cube is " << p->area() << endl;
cout << "The volume of Cube is " << p->volume() << endl;
system("pause");
return 0;
}
Q:typeid运算符
概念:用于获取一个表达式的类型信息
- 对于基本类型来说(int,float,等c++内置类型),类型的数据包含的内容比较简单,主要指数据的类型
- 对于对象来说,类型信息是指对对象所属的类,所包含的成员,所在继承关系等。
typeid 会把获取到的类型信息保存到一个 type_info 类型的对象里面,并返回该对象的常引用;当需要具体的类型信息时,可以通过成员函数来提取。typeid 的使用非常灵活,请看下面的例子(只能在 VC/VS 下运行):
Q:重载和重写
重写可以有两种,直接重写成 员函数和重写虚函数,只有重写了虚函数的才能算作是体现了C++多态性。
重载则是允许有多个同名的函数,函数名字相同,但是参数列表不同,返回型可同可不同。编译器会根据这些函数的不同列表,从而生成一些不同名称的预处理函数,来实现同名函数调用时的重载问题。
重写:派生类中与基类返回值类型,同名和同参数的虚函数重定义,构成虚函数覆盖,也叫虚函数重写
Q:构造函数不能设置为虚函数,析构函数要写成虚函数
白话版: 不能,其实这是一个鸡生蛋还是蛋生鸡的问题,虚函数是为了实现多态是吧?那想要实现多态,首先要先构造一个具有多态性质的对象给你实现吧?那你对象都还没有构造好,怎么去实现多态呢?所以肯定不能把构造函数设置为虚函数。
1、构造函数不能声明为虚函数
1)因为创建一个对象时需要确定对象的类型,而虚函数是在运行时确定其类型的。而在构造一个对象时,由于对象还未创建成功,编译器无法知道对象的实际类型
2)虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数即构造函数了
2、析构函数最好声明为虚函数
如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除指向派生类的基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。
Q:有继承的父子关系,构造和析构一个子类的对象构造和析构顺序
构造时:先执行父类的构造,然后是子类的构造。
析构时:先执行子类的析构,然后是父类的析构。
Q:纯虚函数与抽象类
一般来说,很多时候基类不那个确定函数的实现方法,只能确定函数的功能,纯虚函数是为了实现一个接口,用来规范派生类的行为。即规范继承这个类的程序员必须实现这个函数,
如果基类有虚函数,那么该虚函数就会有对应的函数指针存在于虚函数表,这个函数是实实在在存在的,只是在找到这个函数的时候,需要通过 虚表 这样间接 地来找,所以才叫**“虚”函数**。而 纯虚函数 呢,就是你即使通过 虚表 你也找不到,所以它叫 “纯”虚函数。那么为什么通过 虚表你也找不到呢?因为在虚表中,**纯虚函数的函数地址是 0 ,这和我们写纯虚函数的写法很直观,直接给该函数赋值一个 0 **。而拥有纯虚函数的类就叫做 抽象类。
Q:纯属函数不能被实例化
根据上面的说明,抽象类拥有 纯虚函数,而纯虚函数的函数地址是 0,这个类即使实例化了,也压根找不到对应的成员函数来使用这个实例化对象,那还有什么实例化的意义呢?
因此,抽象类 只是作为接口而存在,具体的实现都交给它的派生类,而派生类要实现抽象类的所有纯虚函数它才能实例化,原因也很简单,就是要保证虚表中没有一个函数指针为0 (NULL)。
Q:接口
C++中使用抽象类来实现Java中类似接口的功能
Q:面向接口编程
软件设计中唯一不断变化的就是需求。所以我们一般希望接口使用者不用去关心具体的接口实现。而面向接口编程可以让我们的接口抽象话。即接口和实现分离。
Q:重载、覆盖、隐藏的区别
该处参考:https://blog.csdn.net/weixin_39640298/article/details/88725073
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
重载是bai指不同的函数使用相同的函数名,但是函数的参数个数或类型不同。调用的时候根据函数的参数来区别不同的函数
覆盖(也叫重写)是指在派生类中重新对基类中的虚函数(注意是虚函数)重新实现。即函数名和参数都一样,只是函数的实现体不一样
1.在同一作用域中,同名函数的形式参数(参数个数、类型或者顺序)不同时,构成函数重载
例如:
class A
{
public:
int func(int a);
void func(int a, int b);
void func(int a, int b, int c);
int func(char* pstr, int a);
};
- 函数返回值类型与构成重载无任何关系
- 类的静态成员函数与普通成员函数可以形成重载
- 函数重载发生在同一作用域,如类成员函数之间的重载、全局函数之间的重载
2.隐藏:指不同作用域中定义的同名函数构成隐藏(不要求函数返回值和函数参数类型相同)。比如派生类成员函数隐藏与其同名的基类成员函数、类成员函数隐藏全局外部函数。
隐藏的特征:
不在同一类中;
函数名相同,若参数不同,不论有无virtual关键字,基类函数都将被隐藏,若函数相同,基类函数没有virtual关键字,则基类函数被隐藏。
3.重写或者覆盖(overide)
重写的定义:派生类中与基类同返回值类型、同名和同参数的虚函数重定义,构成虚函数覆盖,也叫虚函数重写。
需要注意的是,这里有一个特殊情况,即协变返回类型。
class Base
{
public:
virtual A& show()
{
cout<<"In Base"<<endl;
return *(new A);
}
};
class Derived : public Base
{
public:
//返回值协变,构成虚函数重写
B& show()
{
cout<<"In Derived"<<endl;
return *(new B);
}
};
对比覆盖和隐藏,不难发现函数覆盖其实是函数隐藏的特例。如果派生类中定义了一个与基类虚函数同名但是参数列表不同的非virtual函数,则此函数是一个普通成员函数,并形成对基类中同名虚函数的隐藏,而非虚函数覆盖。
隐藏是一个静态概念,它代表了标识符之间的一种屏蔽现象,而覆盖则是为了实现动态联编,是一个动态概念
#include<iostream>
#include<algorithm>
using namespace std;
//基类
class Base{
public:
//基类Fun为重载函数
void Fun(int x){
cout<<"Base::Fun(int x)"<<endl;
}
void Fun(double x){
cout<<"Base::Fun(double x)"<<endl;
}
//基类虚函数
virtual void G(void){
cout<<"Base::G(void)"<<endl;
}
};
//子类
class SubClass:public Base{
public:
//隐藏基类的Fun函数
void Fun(int x){
cout<<"SubClass::Fun(int x)"<<endl;
}
void Fun(double x){
cout<<"SubClass::Fun(double x)"<<endl;
}
//覆盖基类虚函数
virtual void G(void){
cout<<"SubClass::G(void)"<<endl;
}
};
int main(){
Base *base = new Base();
SubClass *subClass = new SubClass();
base = subClass; //基类指针指向子类对象
//测试函数调用
base->Fun(5);
base->Fun(5.00);
base->G();
return 0;
}
分析:
1. 定义一个基类Base,基类内部有两个重载函数Fun和一个虚函数G
2. 子类SubClass继承了基类Base,子类首先隐藏了基类的两个函数Fun并且覆盖了基类虚函数G;
如果基类函数是虚函数,那么子类重新定义就属于覆盖。
如果基类函数不是虚函数,那么子类重新定义属于隐藏基类函数
3. main函数内部,基类指针base指向子类对象指针subClass,然后调用三个函数
(1)base->Fun(5) 输出“Base::Fun(int x)”说明调用的是基类的函数,可以看出通过基类指针并不能动态的调用子类覆盖基类的非虚函数
(2)base->Fun(5.00) 和上面类似
(3)base->G() 输出"SubClass::G(void)" 说明调用的是子类的函数G,可以看出通过基类指针可以动态的调用子类覆盖基类的虚函数
由此可见,多态的实现是通过子类覆盖基类的虚函数,利用基类指针指向不同的子类对象,在运行的时候动态的决定要调用哪个子类虚函数。
Q:final和override关键字
但是我们在写虚函数时,想让派生类中的虚函数覆盖掉基类虚函数,有时我们会不小心写错,造成了隐藏,这不是我们想要看到的结果。所以C++ 11新标准中我们可以使用override关键字来说明派生类中的虚函数。
如果我们使用override标记了某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将报错
class B
{
virtual void f1( int ) const;
virtual void f2();
void f3();
};
class C : B
{
void f1( int ) const override; //正确,f1与基类中的f1匹配
void f2( int ) override; //错误:B没有形如f2(int)的函数
void f3() override; //错误:f3不是虚函数
void f4() override; //错误:B中没有名为f4的函数
};
使用override是希望能覆盖基类中的虚函数,如果不符合则编译器报错。
还能把某个函数指点为 final ,意味着任何尝试覆盖该函数的操作都将引发错误;
class D : B
{
//从B继承 f2() 和 f3(),覆盖 f1( int )
void f1( int ) const final; //不允许后续的其它类覆盖 f1(int)
};
class E : D
{
void f2(); //正确:覆盖从间接类B继承而来的f2
void f1( int ) const; //错误:D已经将 f2 声明成 final
};
final 和 override 说明符出现在形参列表以及尾置返回类型之后。
final 还可以跟在类的后面,意思这个类不能当做其它类的基类。
总结:https://blog.csdn.net/weixin_39640298/article/details/88725073
- 函数重载发生在相同作用域
- 函数隐藏发生在不同作用域
- 函数覆盖就是函数重写。准确地叫做虚函数覆盖和虚函数重写,也是函数隐藏的特例
Q:三种继承方式
公有、保护、私有一句话: 无论哪种继承方式,基类的私有成员派生类都访问不了。
1. 公有继承的时候,基类的公有成员和保护成员都维持现状;
2. 保护继承的时候,基类的公有成员和保护成员都变成保护;
3. 私有继承的时候,基类的公有成员和保护成员都变成私有。
Q:内存泄露
内存泄漏一般指的是没有释放分配的空间。从而导致随着程序的运行,占用内存越来越多。
- 1. new(malloc)出的东西没有delete(free)
- 2. new []出的数组,错误地使用了delete来释放,没有使用delete[]
- 3. 在一些C++使用C函数的调用中。new出来的对象转为void *作为参数传递的时候。没
- 做类型转换。delete void*指针。导致没有进行析构函数的调用。
- 4. 基类的析构函数没有设置为virtual。
- 5. 智能指针相互指向。(weak_ptr解决)
解决办法:
- 1. 上面中的一些错误是理解错误,需要正确的编写代码。比如设置析构函数为虚函数。delete void *指针。
- 2. 一些则是没有使用良好的编程习惯。对于C++来说。我们对指针管理有如下三种办法。
- 1. 使用RAII机制,即用类来封装指针(或者其他资源可以),我们在构造的时候分配空间,在析构的时候释放。
- 2. 使用智能指针管理指针
- 3. 使用boost::intrusive_ptr。需要实现特定的引用计数基类。
野指针:
野指针是指向一段我们不知道内存位置的指针
- 1.比如指向的对象已经被销毁
- 2.指向函数栈中的变量
解决方法
- 1. new的时候要给定一个初值。delete的时候要置为NULL。
- 2. 不要返回指向栈空间的指针。
- 3. 尽量避免在函数中分配空间并返回一个指针。
实际上使用智能指针还是最好的办法
Q:对象管理
不严谨的来说:c++可以分为三种对象管理。
- 1. 对象建立在栈上。由栈管理。栈销毁时,对象销毁。
- 2. 对象建立在堆上。由程序员管理。使用delete销毁。
- 3. 对象建立在堆上。由智能指针管理。自动销毁。
Q:设计模式
https://blog.csdn.net/hechao3225/article/details/71366058
设计模式分为:
1.单例模式
保证一个类只有一个实例,并提供一个访问他的全局访问点,使得系统中只有唯一的一个访问实例
应用:管理资源,日志,线程池
2.工厂模式
工厂模式包括三种:简单工厂模式、工厂方法模式、抽象工厂模式。
Q:智能指针
share_ptr:允许多个指针指向同一个对象,使用计数对资源管理,当引用计数为0时,没有指针指向该资源,资源会释放;
unique_ptr:c++11弃用了auto_ptr,在同一时刻只有一个unique_ptr指向给定的对象,该智能指针是禁止使用拷贝构造函数的 禁止使用赋值运算符重载函数
weak_ptr:指向由一个 shared_ptr 管理的对象,不控制对象生存期,不会改变 shared_ptr的引用计数,当最后一个 shared_ptr 被销毁,对象就会被释放。
weak_ptr 弱智能指针,能看到资源的引用计数,但是不会去用这个引用计数,使用weak_ptr指向资源不会使引用计数增加。
为了配合shared_ptr而引入了weak_ptr,weak_ptr是从share_ptr 或 从另一个weak_ptr对象那里构造,不能直接指向一个新new出来的资源,weaker_ptr可以观察资源的引用情况,他没有重载* 和 -> 运算符。 use_count()函数返回资源的引用计数。 在weak_ptr智能指针类中可以有一个成员函数 lcok(); 该函数的作用是从被观测的对象返回一个可用的shared_ptr对象,可以将这个对象赋值给一个新的shred_ptr智能指针.
Q:左值和右值
Q:深拷贝和浅拷贝
浅拷贝:两个指针指向同一处内存,同一个对象
深拷贝:两个指针各指向一处内存,相当于重新new了一个一模一样的对象
Q:static关键字
静态成员函数:
1,静态成员函数不会传入隐含的this指针,因为他不能访问非静态数据成员和非静态成员函数
2,只能访问静态数据成员和静态成员函数
静态数据成员:
1:静态数据成员被该类的所有对象共享
2.放在只读段,跟全局变量放在一起因此不占用对象的存储空间
3.静态数据成员必须在类之外进行初始化
Q:operator()
Q:虚函数表
参考自大佬链接:https://blog.csdn.net/primeprime/article/details/80776625
操作系统:
Q:死锁的概念
https://blog.csdn.net/esting_tang/article/details/99411818
死锁:多个线程在运行过程中因争夺资源而造成的一种僵局,他们中的一个或者全部都在等待某个资源被释放,由于线程无限期的被阻塞,程序不可能正常终止。
信号量:信号量可以控制资源能被多少个线程访问,指定只能被一个线程访问,就做到了类似锁住,信号量可以指定去获取的访问时间,我们根据这个超时时间,去做一个额外处理。
//死锁程序举例
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
int data = 1;
mutex mt1,mt2;
void th1(){
mt2.lock();
data = data*2;
mt1.lock();//等待th2释放mt1
cout<<data<<endl;
mt1.unlock();
mt2.unlock();
}
void th2(){
mt1.lock();
data =data+1;
mt2.lock();//等待th1释放mt2
mt2.unlock();
mt1.unlock();
}
void main(){
thread t2(th2);
thread t1(th1);
t1.join();
t2.join();
}
上述的例子可以表示为:
关于死锁的其他概念:https://blog.csdn.net/hd12370/article/details/82814348
Q:死锁产生的原因
- 系统资源的竞争
- 进程推进顺序非法
Q:预防死锁的方法
- 资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
- 只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
- 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
- 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)
Q:多线程
1)同步和互斥的概念:现代的多任务操作系统,即同时有大量可调度实体在运行;都需要访问、使用同一个资源;多个任务之间有依赖关系,某个任务的运行依赖于另一个任务
同步:程序必须按照规定的先后次序执行,这种先后次序依赖于要完成的特定的任务
异步:某任务运行其中的一个程序片段时,其他程序不能使用该程序片段,最基本的场景就是:一个公共资源同一时刻只能被一个线程或者进行使用,多个线程或者进程不能同时使用公共资源。
2)互斥锁:互斥锁用于控制多个线程对他们共享资源互斥访问的一个信号量。也就是说是为了避免多个线程在某一时刻同时操作一个共享资源。互斥锁只有两种状态,上锁和解锁
如果一个线程锁住了一个互斥量,没有其他线程可以在同一时间锁定这个互斥量;如果试图去锁定,这个线程就会被挂起(不占用任何cpu资源),直到第一个线程解除对这个互斥量的锁定为止。
3)条件变量,通常和互斥锁一起使用,当条件满足的时候,线程通常解锁并等待该条件发生变化,一旦另一个线程修改了环境变量,就会通知相应的环境变量唤醒一个或者多个被这个条件变量阻塞的线程。这些被唤醒的线程将重新上锁,并测试条件是否满足。一般来说条件变量被用于线程间的同步;当条件不满足的时候,允许其中的一个执行流挂起和等待。
4)读写锁:读写锁和互斥量类似,不过读写锁允许更改的并行性,也叫共享互斥锁,互斥量要么是锁住状态,要么是不加锁状态,而且一次只能有一个线程可以对其加锁,读写锁可以有三种状态,读模式下加锁状态,写模式写加锁状态,不加锁状态;
一次只能有一个线程占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁
读写锁的特点:
- 如果有其他线程读数据,则允许其他线程执行读操作,但不允许写操作
- 如果有其他线程写数据,则其他线程都不允许读、写操作。
5)自旋锁:是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环
Q:临界资源
http://c.biancheng.net/cpp/html/2596.html
虽然多个进程可以共享系统中的各种资源,但其中许多资源一次只能为一个进程所使用,我们把一次仅允许一个进程使用的资源称为临界资源
对临界资源的访问必须互斥的进行,在每个进程中,访问临界资源的那段代码称为临界区
Q:同步
也称互相制约关系,他是指为了完成某种任务为建立的多个进程要协调他们的工作次序而等待,产生的制约关系;
例如,输入进程A通过单缓冲向进程B提供数据。当该缓冲区空时,进程B不能获得所需数据而阻塞,一旦进程A将数据送入缓冲区,进程B被唤醒
Q:阻塞
互斥亦称间接制约关系。当一个进程进入临界区使用临界资源时,另一个进程必须等待, 当占用临界资源的进程退出临界区后,另一进程才允许去访问此临界资源
Q:信号量
信号量是一种功能比较强的机制,可以用来解决互斥和同步的问题,它只能被两个标准的原语wait(S)和signal(S)来访问,也可以记为“P操作”和“V操作”。
P操作:wait(S)
V操作:signal(S)
抽象的来讲,信号量的特性如下:信号量是一个非负整数(车位数),所有通过它的线程/进程(车辆)都会将该整数减一(通过它当然是为了使用资源),当该整数值为零时,所有试图通过它的线程都将处于等待状态。在信号量上我们定义两种操作: Wait(等待) 和 Release(释放)。当一个线程调用Wait操作时,它要么得到资源然后将信号量减一,要么一直等下去(指放入阻塞队列),直到信号量大于等于一时。Release(释放)实际上是在信号量上执行加操作,对应于车辆离开停车场,该操作之所以叫做“释放”是因为释放了由信号量守护的资源。