C++基础
1. C++能做什么
2. 面向对象-基本概念
对象是对客观事物的抽象,类是对对象的抽象
对象(Object)是类(class)的一个实例(Instance)
3. 封装-基本概念
封装是指一种将抽象性函式接口的实现细节部分包装、隐藏起来的方法。
C语言的结构体不具有封装属性。
4. 继承/派生-基本概念
类的继承,是新的类从已有类那里得到已有的特性,或从已有类产生新
类的过程就是类的派生。原有的类称为基类或父类,产生的新类称为派生类或子类。
5. 继承/派生-注意问题
1. 用一个类做基类,相当于声明一个该类的对象,所以一定要有类的定义,只有声明是不行的
class emploee; //只有声明 没有定义
class manager:public emploee{}; //不行, 没有emploee定义
2. 派生类可以使用基类中所有public和protected的成员
3. 类对象的构造是自下向上的,首先是基类,而后是成员,再后才是派生类本身,类对象的销毁刚好相反。
4. *构造函数和析构函数无法继承,因此在派生类中需要自己对基类的构造函数进行调用。
5. 构造函数原则:
1)子类没有定义构造方法,系统默认调用父类的无参数构造方法。
2)子类定义了构造方法(不论无参数还是带参数),若未显示调用父类的构造方法,
首先执行父类无参数的构造方法,然后执行自己的构造方法。
3)子类定义了构造方法(不论无参数还是带参数),若未显示调用父类的构造方法,
但是父类只定义了有参构造函数,则会出错(如果父类只有有参数的构造方法,则子类
必须显示调用此带参构造方法。
4)子类在构造函数中显示带哦用父类带参数的构造方法,需要初始化父类成员对象的方法。
6. 重载/多态
多态指发出同样的消息被不同类型的对象接收时有可能导致完全不同的行为。
多态性是一种实现“一种接口,多种方法“的技术
重载也是一种多态,不过是一种特殊形态的多态,是编译时决定的静态多态。(编译时多态)
多态的三大特征:重写、继承、父类指针指向子类对象(运行时多态)
函数重载:在C++程序中 ,将语义、功能相似的几个函数用同一个名字表示。
重载发生在同一个作用域中。
运算符重载:在C++语言中,用关键字operator加上运算符来表示函数。
虚函数:是C++实现统一接口的途径,在基类中定义具有通用接口(泛化)的虚函数,在派生类中将虚函数实现(特化)。
7. 为什么使用函数重载?
1. 便于记忆,提高函数易用性。
2. 类的构造函数需要重载机制。
8. 重载如何区分,靠参数,编译器根据参数为每个重载函数分配不同的标识符。
9. 运算符重载的规则
1)只能重载C++中已有的运算符
2)类属关系运算符“.”、作用域分辨符“::”、成员指针运算符“*”、sizeof运算符
和三目运算符“?:”不能重载
3)重载之后的运算符的优先级和结合性都不能变,单目运算符只能重载为单目运算符,双目运算符
只能重载为双目运算符。
4)运算符重载后的功能应该与原有功能相类似。
5)重载运算符含义必须清楚,不能有二义性。
10. 虚函数与重载的关系
1)虚函数的重载要求函数名、返回类型、参数个数、参数类型以及参数的顺序都与基类中原型完全相同,
不能有任何的不同。
2)一般函数的重载,只要函数名相同即可,函数的返回类型以及所带的参数可以不同。
11. 虚函数规则
1)只有成员函数才能声明为虚函数,因为虚函数仅适用于有继承关系的类对象,所以普通函数不能声明为虚函数。
2)虚函数必须是非静态成员函数,因为静态成员函数不受限于某个对象。
3)内联函数不能声明为虚函数,因为内联函数不能在运行中动态的确定其位置。
4)构造函数不能声明为虚函数,多态是指不同的对象对同一消息有不同的行为特性。
虚函数作为运行过程中多态的基础,主要是针对对象的,而构造函数是在对象产生之前
运行的,因此,虚构造函数是没有意义的。
5)析构函数可以声明为虚函数,析构函数的功能是在该类对象消亡之前进行一些必要的清理工作,析构函数没有类型,
也没有参数,和普通成员函数相比,虚析构函数情况略微简单些。
12. 纯虚函数
概念:一个在基类中没有定义具体操作内容的虚函数,要求各派生类根据实际需要定义自己的实现内容。
声明形式为:virtual <函数类型> <函数名> (参数表) = 0
抽象类:
主要作用是通过它为一个类族建立一个公共接口,使他们能够更有效的发挥多态特性。
一个抽象类至少带有一个纯虚函数。
13. 纯虚函数规则
1)抽象类只能用作其他类的基类,不能建立抽象类对象
抽象类处于继承层次结构的较上层,一个抽象类自身无法实例化,只能通过继承机制,生成抽象类的非抽象派生类,然后再实例化。
2)抽象类不能用作参数类型、函数返回值或显示转换的类型。
3)可以声明一个抽象类的指针和引用。
14. 单例对象
1. 确保一个类只有一个实例被建立
2. 提供了一个对象的全局访问点,被所有程序模块共享
为了防止在外部调用累的构造函数而构造实例,需要将构造函数的访问权限标记为protect或private,最后,
需要提供要给全局访问点,就需要在类中定义一个static函数,返回在类内部唯一构造的实例。
15. 静态对象
局部静态对象:局部静态变量在程序第一次执行定义语句时初始化,直到程序终止被销毁,其生命周期贯穿函数
调用及之后的时间。
int count_calls(){
static int ctr = 0; //调用结束后仍然存在
renturn ++ctr;
}
类的静态成员:在类的成员变量前加上static,可以使该成员与类关联在一起,从而不与任何对象绑定,
被该类及其派生类的所有对象共享。
类的静态成员不在创建类的对象时被定义,因此也不是由类的构造函数初始化。一般来说在类的外部定义和初始化类的静态成员。
class bass{
public:
static int _num; //类内声明
}
int base::_num = 0; //类外定义
class derived:public base{
};
main()
{
base a;
derived b;
a._num++;
cout << "base class static data number _num is" << a._num << endl;
b._num++;
cout << "derived class static data number _num is" << b._num << endl;
}//结果为1, 2; 可见派生类与基类共用一个静态数据成员
静态成员变量可以是类本身的对象,普通成员变量不可以;
class base{
public:
static base _object1; //正确,静态数据成员
base _object2; //错误
base *pObject; // 正确,指针
base &mObject; // 正确,引用
}
静态成员变量可以作为成员函数的默认参数,普通变量不可以
class base{
public:
static int _staticVar;
int _var;
void foo1(int i = _staticVar);//正确, _staticVar为静态数据成员
void foo2(int i = _var);//错误,_var为普通数据成员
}
16.静态成员函数
1)静态成员函数不与类的对象相关联,即函数内不含this指针,因此只能访问类的静态成员变量与静态成员函数,不能调用类内非静态成员。
2)与非静态成员函数相比,静态成员函数不是需要等待类的对象初始化,在没有对象被初始化时也可以直接通过类进行调用。
3)静态成员函数的地址可用普通函数指针存储,而普通成员函数地址需要用类成员函数指针来存储。
class base{
static int fun1();
int fun2();
};
int (*pf1)() = &base::func1;//普通函数指针
int (base::*pf2)() = &base::func2;//成员函数指针
17. 指针的基本概念
1)引用是某一变量的一个别名,对引用的操作与对变量直接操作完全一样。
2)指针是指向另一种类型的复合类型,保存制定对象的地址。通过指针对象可以实现对其他对象的间接访问,这一点与引用类似。
3)指针依赖于指向的类型,其不仅所指向对象在内存空间的位置,还包含所指向对象占用的内存空间大小。
4)相对于引用,指针本身是一个对象,声明时可以不赋初值,也可以在生命周期内更改所指向的对象。
18. 指针常量与常量指针
指针常量:指针里面所存储的内容(内存地址)是常量,不能改变
声明格式为:数据类型 *const 指针变量
常量指针:指针指向的是常量,他指向的内容不能发生改变
声明格式为:数据类型 const *指针变量
19. 数组指针与指针数组
数组指针:即指向数组的指针,如 int(*a)[4]
指针数组:用于存储指针的数组,也就是数组元素都是指针,如 int* a[4]
数组指针:
一般来说编译器会直接将数组转换为指针,即可以将数组名直接当作数组第一个成员的指针来使用。
指针数组;
由于指针也是对象,因此可以声明指针的数组,用来保存一列相同对象的集合。
定义指向int类型的指针的数组,其中[]优先级更高,即p为一个n维数组,其次为*,即为指针数组,最后为int,即为指向int类型的指针的数组。
结合数组指针的内容,可以将二位数组的每一行当作指针,则二维数组与指针的数组类似。
int *p[3];
int a[3][4];
for (int i = 0; i < 3; i++)
p[i] = a[i];
20. 指针参数
1)指针可以作为函数的参数,传递给函数体内的代码使用。当指针作为函数形参时,其行为
与其他非引用类型一样,实参的值会被拷贝给形参。由于指针可以间接访问指向的对象,
因此通过被拷贝的形参可以指向实参所指向的对象,函数体中对所指对象的修改可以被保留。
2)当所指的对象占用内存控件较大时,通过指针形参可以避免对象的拷贝操作,转为指针的拷贝操作,提高效率。
21. 函数指针
函数指针指向函数而非对象,其指向的类型由函数的返回类型和形参类型共同决定。
bool (*pf)(int, int*); //定义了一个函数指针,指向返回类型为bool,形参为int和int*的函数。
函数名会被自动当作函数指针来处理
bool chk(int, int*){return false;}
pf = chk;
函数指针作为指针,可以成为其他函数的形参,也可以作为其他函数的返回值。
int compare(int, int, int(*pf)(int, int)); //定义了函数compare(int, int, int(*pf)(int, int))
int (*f1(int))(int*, int);//定义了函数f1, 形参为int, 返回类型为int(*)(int*, int)
22. this指针
在每个成员函数中都包含了一个特殊的指针,称为this指针,他是指向本类对象的指针,他的值是当前被
调用成员函数所在对象的起始地址。
23. 内存分区
栈(stack)
局部变量存储区,函数内部、实参的局部变量,由编译器负责分配,函数结束,栈变量失效。
堆(heap)
用户自行分配,使用new/delete、deelte[]进行释放(malloc\calloc\relloc\ free), 成对出现,申请则释放
否则造成内存泄露。
全局区/静态区(global static area)
全局变量和静态变量存放区,程序一经编译好,该区域便存在。全局变量和静态变量编译器会给这些变量自动初始化赋值。
由于全局变量一直占据内存空间且不易维护,推荐少用。程序结束时释放。
常量区
这是一块比较特殊的存储区,专门存储不能修改的常量,如字符串常量。
代码区
函数体的二进制代码。
24. 堆栈区别
栈 堆
存储对象 局部变量、函数参数等 malloc或new出来的对象
大小 小,vc缺省大小为2M 大,受限于机器的虚拟内存大小
速度 快 相对栈来说较慢
结构 先进后出 无严格的数据结构
内存连续 内存连续,不会出现碎片问题 频繁的new/delete等操作势必会造成内存空间的不连续,从而造成大量的碎片,使程序的效率降低
管理 操作系统维护 使用者维护
25. 内容操作注意事项
1)用malloc或new申请内存之后,应该立即检查指针值是否为NULL,防止使用指针值为NULL的内存
2)不要忘记为数组或动态内存赋初值(比如calloc比malloc就要好),指针初始化为NULL(c++为0)
3)避免数组或指针下标越界,特别当心发生 多1 或者 少1 的操作
4)动态内存的申请和释放必须配对,防止内存泄露,具体为malloc/calloc/realloc和free配对,new和delete以及delete[]配对
5)用free或delete释放内存后,应立即将指针设置为NULL(C++中为0),防止 野指针 悬垂指针
6)类的内存大小 c++中空类/结构体的内存大小为1字节
26. 内存池 一种内存分配方式,提高内存的使用效率
在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用,当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再申请新的内存。
解决频繁调用new/delet产生的性能消耗
减少内存碎片
27. 内存池分类
线程安全角度
单线程内存池:整个生命周期只被一个线程使用,因此不需要考虑互斥访问的问题。性能更高
多线程内存池:可能被多个线程共享,因此则需要在每次分配和释放内存时加锁。适用范围更广
可分配单元大小
固定内存池:应用程序每次从内存池中分配出来的内存单元大小事先已经确定,是固定不变的。
可变内存池:每次分配的内存单元大小按需变化。
28. STL容器
序列容器:线性结构的可序群集,容器内的元素按顺序进行存放,可按位置存储和访问,元素排列次序与元素无关,
而是由元素添加到容器内的次序决定的。
vector、list、deque、string
关联容器:非线性结构,更准确说是二叉树结构,各元素之间没有严格的物理上的顺序关系,即元素
在容器中并灭有保存元素植入容器时的逻辑顺序。以键值的方式保存数据,把关键字或值关联起来保存键值对容器,元素按键进行排序。
set、hash_set、multiset、hash_map、map、hash_multiset、multimap、hash_multimap
29. 迭代器
迭代器用来访问一个容器类的所包含的全部元素,其行为像一个指针。
迭代器是个所谓的复杂的指针,具有遍历复杂数据结构的能力。其下层运行机制取决于遍历的数据结构。
30. vector
vector可采用连续存储空间来存储元素。可以采用下标对vector元素进行访问
vector在访问元素时候更加高效,在末尾添加和删除元素相对高效。对于其他不在末尾的删除和插入操作,效率更低。
遍历方法;下标法 和 迭代器
31. list
一种序列型容器,是一个线性链表结构,他的数据由若干个节点构成,无需分配制定的内存大小且可以任意伸缩,这是因为
他存储在非连续的内存空间中,并且由指针将有序的元素链接起来。
由于是链表结构,list随机检索的性能非常的不好,但是他可以迅速的在任何节点进行插入和删除操作。
遍历方法: 迭代器
32. map
一种关联型容器,一个键值对(key/value)容器,map与multimap差别仅仅在于multimap允许一个键对应多个值。
map内部是自建一颗红黑树(一种非严格意义上的平衡二叉树),这棵树具有对数据自动排序的功能,所以在map内部
所有的数据都是有序的。其“键“子啊容器中不可以重复,且按一定顺序排列,可以修改实值,但是不能修改key
map的检索操作比vector慢,比list快。
遍历方法:迭代器法 和 赋值
删除元素:注意迭代器的位置
33. 文件读写
C操作 ANSI C提供的一组标准库函数实现 fopen、fseek、fread、fwrite、fclose
MFC操作 MFC提供的CFile类
WINAPI win32 api函数 CreateFile
C++操作 C++提供的文件操作类ofstream、ifstream、fstram
34. 常引用、常对象和对象的常成员
http://www.jizhuomi.com/software/68.html
1)常引用
用const声明的引用就是常引用。常引用所引用的对象不能被更改。我们经常见到的是常引用作为函数的形参,这样不会发生对实参的误修改。
常引用的声明形式为:const 类型说明符 &引用名。
#include<iostream>
using namespace std;
void show(const double& r);
int main()
{
double d(9.5);
how(d);
return 0;
}
void show(const double& r)
//常引用作形参,在函数中不能更新r所引用的对象。
{
cout<<r<<endl;
}
2)常对象
所谓常对象,是指数据成员在它的生存期内不会被改变。定义常对象时必须对其进行初始化,并且不能改变其数据成员的值。
常对象的声明形式为:类名 const 对象名 或者 const 类名 对象名。
class A
{
public:
A(int i,int j) {x=i; y=j;}
...
private:
int x,y;
};
A const a(6,8); //a是常对象,不能被更新
说明: 如果程序中出现对常对象的数据成员修改的语句,编译器会报错。
一般修改对象的数据成员有两种途径,一种是通过对象名访问公有数据成员并修改其值,而常对象的数据成员是不能被修改的;
另一种是类的成员函数修改数据成员的值,而常对象不能调用普通的成员函数。可是这样的话,常对象就只剩数据,没有对外的接口了,
这就需要为常对象专门定义的常成员函数了。
3)类的常成员函数
类中用const声明的成员函数就是常成员函数。
常成员函数的声明形式为:类型说明符 函数名(参数表) const。
a.常成员函数在声明和实现时都要带const关键字;
b.常成员函数不能修改对象的数据成员,也不能访问类中没有用const声明的非常成员函数;
c.常对象只能调用它的常成员函数,不能调用其他的普通成员函数;
d.const关键字可以被用于参与对重载函数的区分,比如,如果有两个这样声明的函数:void fun(); void fun() const;,则它们是重载函数。
#include<iostream>
using namespace std;
class R
{
public:
R(int r1, int r2) { R1=r1; R2=r2; }
void print();
void print() const;
private:
int R1,R2;
};
void R::print()
{
cout<<R1<<":"<<R2<<endl;
}
void R::print() const
{
cout<<R1<<";"<<R2<<endl;
}
int main()
{
R a(5,4);
a.print(); //调用void print()
const R b(20,52);
b.print(); //调用void print() const
return 0;
}
上面的R类中声明了两个同名函数print,第二个是常成员函数。
在main函数中定义了两个对象a和b,b是常对象,通过a调用的是没有用const声明的函数,而通过b调用的是用const声明的常成员函数。
4)类的常数据成员
类的数据成员也可以是常量和常引用,用const声明的数据成员就是常数据成员。在任何函数中都不能对常数据成员赋值。构造函数对常数据成员初始化,只能通过初始化列表。
#include<iostream>
using namespace std;
class A
{
public:
A(int i);
void print();
const int& r;
private:
const int a;
static const int b; //静态常数据成员
};
const int A::b=20;
A::A(int i):a(i),r(a) {}
void A::print()
{
cout<<a<<":"<<b<<":"<<r<<endl;
}
int main()
{
//建立对象a和b,并以50和10作为初值,分别调用构造函数,通过构造函数的初始化列表给对象的常数据成员赋初值
A a1(50),a2(10);
a1.print();
a2.print();
return 0;
}
此程序的运行结果是:
50:20:50
10:20:10
35. C++ const修饰函数、函数参数、函数返回值
https://www.runoob.com/w3cnote/cpp-const-keyword.html
https://www.cnblogs.com/wjxx836/p/4440823.html
36. 互斥锁、递归锁、读写锁和自旋锁区别
https://www.cnblogs.com/evenleee/p/11309156.html
互斥锁
共享资源的使用是互斥的,即一个线程获得资源的使用权后就会将改资源加锁,使用完后会将其解锁,所以在使用过程中有其它线程想要获取该资源的锁,那么它就会被阻塞陷入睡眠状态,直到该资源被解锁才会别唤醒,如果被阻塞的资源不止一个,那么它们都会被唤醒,
但是获得资源使用权的是第一个被唤醒的线程,其它线程又陷入沉睡。
递归锁
同一个线程可以多次获得该资源锁,别的线程必须等待该线程释放所有次数的锁才能获得。
读写锁
读写锁拥有读状态加锁、写状态加锁、不加锁三种状态。只有一个线程可以占有写状态的锁,
但可以多个线程同时占有读状态锁,这也是它可以实现高并发的原因。
当其处于写状态锁下,任何想要尝试获得锁的线程都会被阻塞,直到写状态锁被释放;
如果是处于读状态锁下,允许其它线程获得它的读状态锁,但是不允许获得它的写状态锁,
当读写锁感知到有线程想要获得写状态锁时,便会阻塞其后所有想要获得读状态锁的线程。
所以读写锁非常适合资源的读操作远多于写操作的情况。
读写锁三个特征:
多个读者可以同时进行读
写者必须互斥,只允许一个写者写,也不能读者写者同时进行
写者优先于读者,一旦有写者,则后续读者必须等待,唤醒时优先考虑写者
自旋锁
自旋锁是一种特殊的互斥锁,当资源被加锁后,其它线程想要再次加锁,
此时该线程不会被阻塞睡眠而是陷入循环等待状态(不能再做其它事情),
循环检查资源持有者是否已经释放了资源,这样做的好处是减少了线程
从睡眠到唤醒的资源消耗,但会一直占用CPU资源。适用于资源的锁被持
有的时间短,而不希望在线程的唤醒上花费太多资源的情况。
自旋锁的目的
自旋锁的实现是为了保护一段短小的临界区操作代码,保证这个临界区的操作是原子的,
从而避免并发的竞争冒险。在Linux内核中,自旋锁通常用于包含内核数据结构的操作,
你可以看到许多内核数据结构中都嵌入有spinlock,这些大部分就是用于保护它自身被操作的原子性,
在操作这样的结构体时都经历这样的过程:上锁-操作-解锁。
如果内核控制路径发现自旋锁“开着”(可以获取),就获取并继续自己的执行。
相反,如果内核控制路径发现锁由运行在另一个CPU上的内核控制路径“锁着”,
就在原地“旋转”,反复执行一条紧凑的循环检测指令,直到锁被被释放。自旋锁是循环检测“忙等”,
即等待时内核无事可做,进程在CPU上保持运行,所以它的临界区必须小,且操作过程必须短。不过,
自旋锁通常非常方便,因为很多内核资源只锁1毫秒的时间片段,所以等待自旋锁的释放不会消耗太多CPU的时间。