1. 关键字的作用:
extern:
1. 用在变量和函数前面,表示定义在其他文件或模块中;
经常出现extern的变量和函数,和声明的不一样,一般解决方法是,放到头文件中加extern,其他模块引用头文件
2. extern C
C++为了解决函数多态的问题,编译时会将函数名和参数合起来生成一个中间名称,extern C告诉编译器保持名称不变
static:
1. static变量和函数只在当前文件中可见
2. 静态全局变量,在程序开始运行时初始化,未初始化的会自动初始化为0,在静态(全局)数据区分配内存
静态局部变量,在程序第一次执行到时初始化,以后再调用不会初始化,再全局数据区分配内存,作用域为函数内部
C++的static成员属于类成员,所有类对象共享,没有this指针,可以通过对象或类名引用
volatile:
是一种类型修饰符,表示变量是“易变”的,随时可能发生变化,用于防止编译器优化时,将变量从内存放入寄存器,如果是非volatile变量, 多线程时,有可能一个线程访问内存,一个线程访问寄存器,会造成错误,而volatile变量,每次使用时都首先从内存中获取。
但是,volatile并不能解决多线程并发问题,两个非volatile变量,或一个非volatile一个volatile变量,编译器可能会优化代码执行顺序,两个volatile变量,编译器不会优化指令顺序,但是CPU却可能乱序执行,因此对于多线程,还是使用互斥、读写锁之类的方法,来保证线程同步。
const:
const int n = 10 const修饰的变量具有常属性,不能被修改,必须在声明时初始化
const int * n = &a 或 int const * n = &a const在*之前,常量指针,指针指向的内容是常量,不能改变内容
int * const n = 1 const在*之后,指针常量,指针本身是个常量, 不能指向其他地址
const int &b = a; const修饰引用,可以修饰函数的参数,返回值
const成员变量,只能在构造函数的初始化列表中赋值,不能在构造函数中赋值,在单个对象内是常量,但是对于类的多个对象,是可以为不同值的
const成员函数,int getvalue() const {}表示函数内不能修改类对象的所有成员变量,也不能调用任何非const成员函数
const修饰类成员,则位类常量对象,所有成员都不能改变,也不能调用非const成员函数
const重载 void func(){} 和 void func() const{} 是两个函数
const_cast运算符用来修改类型的const或volatile属性
2. const全局变量和宏的区别
const常量便于类型检查,宏没有数据类型,防止意外修改,
const节省空间,通常情况下,编译器不为其分配内存,只是将其保存在符号表中避免不必要的内存分配,给出的是对应的内存地址, 而宏给出的是立即数
3. 内联函数和宏的区别
宏不是函数,是在编译的预处理阶段,将程序中的相关字符串简单替换
内联函数是将代码直接插入到调用处,减少了普通函数调用时的资源消耗
内敛函数可以在运行时调试,宏不行
编译器会对内敛函数做类型检查或自动类型转换,宏定义不会
类中声明同时定义的函数,自动转为内联函数
4. new和malloc的区别
1. new是C++的操作符,malloc是库函数
2. 申请的内存的位置,new在自由存储区上,自由存储区是C++基于new操作符的一个抽象概念,可以是堆或静态存储区,malloc是在内存池,也就是堆上分配内存空间
3. 返回类型,成功时,new返回对象类型指针,不需要类型转换,malloc返回void*,需要类型转换,失败时new抛出bac_alloc异常,malloc返回NULL
4. 参数,new不需要制定分配内存的大小,会根据类型自动计算,malloc需要指定分配内存的大小
5. new会调用构造函数,malloc不会,构造函数三步,new申请内存,调用构造函数,返回类对象指针
6. 对数组的处理, new[] 给出数组元素个数,malloc需要指定数组所有元素内存总和
7. new和delete可以重载,可以基于malloc实现,malloc/free不能重载,不能调用new
8. 重新分配内存, malloc分配的空间不足时,可以调用realloc实现内存扩充,new没有类似的功能,realloc先判断当前指针指向的地址是否有足够的连续内存空间,如果有则元原地扩充内存,返回原地址,如果连续空间不够,则重新分配指定大小的空间,将原数据拷贝到新空间,并释放原空间,返回新地址
5. C++多态
1. C++的多态:在基类的函数前加virtual关键字,在子类中重写该函数,运行时根据对象的实际类型来调用相应的函数,父类对象调用父类函数,子类对象调用子类函数。
2. 多态的原理就是虚函数和动态绑定:
1>有虚函数的类都有一个一维虚函数表,类对象都有一个指向虚表开始的指针,续表和类对应,虚表指针和类对象对应。同一个类的多个对象,虚表是相同的。虚表存放在全局数据区,不占用类对象的内存空间。每个类对象都包含一个虚表指针,不同的对象,虚表指针不同。计算类对象的大小,需要注意是否有虚函数表指针。虚表指针在构造函数中完成初始化。
2>动态绑定是利用虚函数,在子类中重写父类的函数, 运行时根据对象的实际类型调用相应的函数
3. 纯虚函数是virtual void func() = 0; 纯虚函数只有声明没有定义,用来规范派生类的接口
4. 抽象类是包含纯虚函数的类,不能创建实例,但是可以声明指向派生类的指针或引用
5. 构造函数不能是虚函数,析构函数可以是虚函数,并且在包含虚函数的类中,析构函数一般都定义为虚函数,是为了防止只释放基类资源,比如使用基类指针指向子类对象时,如果析构函数不是虚函数,则delete基类指针时,调用的是基类的析构函数,不调用子类析构函数,可能造成内存泄漏。
6. 构造函数可以抛出异常,异常后不会调用析构函数,可能造成内存泄漏,析构函数不应该抛出异常,会直接导致当前线程退出,解决办法是在析构函数中使用try catch吞掉异常
7. 构造函数和析构函数从语法上可以调用虚函数,但都不适用动态联编,调用的是构造函数和析构函数本身所在类的版本
8. 动态多态就是利用虚函数,在基类函数前加virtual,在子类中重写该函数,运行时根据对象的实际类型调用相应的函数,静态多态,是各个类不需要有相同的基类,只需要有相同的接口,然后将接口定义为模板,在编译时确定需要什么对象,就将对象作为模板的实参
9. 必须在构造函数初始化列表里初始化的有:1) 成员是一个类对象,且这个类没有默认构造函数,只有带参数的构造函数,2)引用或const修饰的成员,3)子类初始化父类的私有成员时,必须也只能在初始化列表中显示的调用父类构造函数
必须在构造函数初始化列表里初始化的数据成员有哪些
6. sizeof结构体、带虚函数的类、指针、字符串、数组
sizeof基本数据类型,32位系统中,int long都是4字节,double是8字节,64位系统中int为8字节
sizeof字符串,需要考虑字符串结尾的'\0'
sizeof指针,32位中为4字节,64位为8字节
sizeof数组,是数组实际占用的字节总数,注意不是数组元素个数
sizeof结构体,考虑字节对齐,#pragma pack(n)可以改变字节对齐数,空结构体大小为由编译器决定,一般为1
sizeof类,和结构体一样,考虑字节对齐, 成员函数都不计算大小,只计算成员变量, 当类中有虚函数时,编译器会创建虚函数表,不计入类大小,但是会为每个对象保存一个虚表指针,因此带有虚函数的类,计算大小时需要加虚表指针的大小
7. 指针和引用的区别
1. 指针是一个实体,占用内存空间,引用是变量的别名,从语言的角度不占内存,从汇编上看占用内存
2. 指针可以不初始化,可以改变,引用必须在声明时初始化,且不能改变
3. 指针可以为NULL,使用前必须判断是否为NULL,不存在空引用
4. sizof指针得到指针本身的大小,sizeof引用得到引用指向的变量的大小
5. 指针和引用的自增++,运算意义不一样
6. 智能指针,auto_ptr是C99的内容,share_ptr,weak_ptr是C11的新内容,引用计数
9. C++内存管理
1. 栈,编译器自动分配释放,windows默认2M,linux默认8M栈,ulimit -a 查看当前设置,ulimit -s 16384设置为16兆,可以修改系统配置文件
2. 堆,由程序员使用malloc、free等分配释放,是操作系统的属于,由操作系统维护,不连续的空间,理论上是硬盘的大小
3. 自由存储区,是C++使用new、delete分配和释放对象的抽象概念,基本上所有的C++编译器都是用堆实现自由存储
4. 静态/全局数据区,分为初始化区和未初始化区两部分,大小2G,未初始化时系统自动置0
5. 常量区,字符串常量等
6. 代码区
static全局变量和static局部变量,都存放在静态/全局区
const全局变量存放在静态/全局数据区,编译器最初保存在符号表中,第一次使用时分配内存,程序结束时释放,const局部变量放在栈区,代码块结束时释放。
全局变量存放在静态/全局数据区,编译时分配内存,程序结束时释放。
局部变量存储在栈中,代码块结束时释放
内存对齐的原则:
1)结构体成员,第一个从offset=0处开始,以后每个成员都从其自身大小的整数倍开始存放,如int成员必须从0,4,8开始
2)结构体中的结构体成员,要从其内部“最宽的基本类型“的整数倍开始存放
3)sizeof结构体,必须是其内部最宽基本类型的整数倍
4)sizeof联合体,是其内部最大成员的大小
结构体成员的顺序不同,sizeof的结果可能不同
内存对齐的作用:
1)平台原因,有些硬件平台只能从特定地址读取特定类型的数据,便于移植
2)性能原因,内存对齐后,CPU的内存访问速度会大大提升
10. STL标准库
string:字符串类,封装char*,不用考虑内存释放
三种遍历方式:数组方式str[i](可能越界异常)、at方法str.at(i)(越界会抛出异常)、迭代器string::iterator
获取c字符指针const char* p = str.c_str();
字符串拼接:重载+操作符s3 = s1 + s2;append方法s3 = s1.append(s2)
查找:size_t index = str.find("hello", 0); 替换 str.replace(index, len, "welcome")
删除:string& erase(pos, n) 插入:string& insert(pos, str)
vector:封装数组,随机存取快,支持[]和at访问,在中间插入时,需要移动元素,效率慢
基本方法:back() front() push_back() pop_back()
初始化:vector<int> v1; v1.push_back(1); vector<int> v2 = v1; vector<int> v3(v1.begin(), v1.end()); vector<int> v4(3, 9);
遍历:支持[]和at(),迭代器支持逆向,begin() - end() rbegin() - rend(); 遍历时删除要注意 it = v1.erase(it)
list:封装双向链表,没有重载[],随机存取效率为O(n),比vector慢,插入删除效率比vector高,因为不需要移动元素,
queue:底层一般用list或deque实现,FIFO先进先出,不用vector,是因为容量有限制,扩容耗时
priority_queue:优先级队列,底层一般用vector为容器,使用堆为处理规则,
stack:底层一般用list或deque实现,FILO先进后出,不用vector,是因为容量有限制,扩容耗时
deque:deque<int> dq; 双端队列,内部维护一段连续的空间,其中每个元素都是一个指针,指向另一块连续的空间,存放实际的数据,支持随机访问,由于是若干段不连续空间组成,因此随机访问和遍历的效率比vector慢。两端都可以插入删除数据。
map/multimap:底层数据结构为红黑树,键值对,有序,不重复map,可重复multimap
map的key如果是自定义类型,则需要重载<操作符。因为是按key有序存储的
set/multiset:底层数据结构为红黑树,集合,有序,不重复set,可重复multiset
hash_set/hash_multiset:底层数据结构为hash表,无序,不重复hash_set,可重复hash_multiset
hash_map/hash_multimap:底层数据结构为hash表,无序,不重复hash_map,可重复hash_multimap
红黑树:是一种自平衡的二叉查找树,AVL是完全平衡树,节点的左右子树高度差不超过1,红黑树只要求局部平衡。
1)每个节点都是红色或黑色
2)根节点是黑色
3)每个为NULL的叶子节点是黑色
4)如果一个节点是红色,则它的子节点必须是黑色的
5)从一个节点到这个节点的的叶子节点的所有路径上,黑色节点的个数相同
stl常用算法:
1. sort: sort(begin, end, compare) sort(a, a+20)
数据量大时,采用快速排序,将数据分段归并排序,对于每一段数据,如果数据量较小(比如小于16),为了避免快排递归调用带来的额外负荷,就采用插入排序,如果递归层次太深,就采用堆排序。
关系型容器,如map、set等,采用红黑树,具有自动排序功能,不需要sort
序列型容器,stack、queue、priority_queue有特定出入口,不允许用户排序,剩下vector和deque适合排序
2. qsort:sort(in,100,sizeof(in[0]),cmp) 快速排序
iterator中remove和erase的区别
vector的remove是把等于value的元素放到vector的尾部,size不变, erase是删除某个位置或区域的元素,size变小
list的remove是删除value元素,并释放资源,erase是删除pos位置的元素
set只有erase,没有remove,有三种删除方法erase(pos)、erase(val)、erase(begin, end)
11. 原子操作
原子操作是指一个操作(可能包含多个指令)是不可中断的,即使在多线程中,一个操作一旦开始,就不会被其他线程打断。
i++和++i都不是原子操作,系统操作一个变量包括三步:1读,从内存读到寄存器,2改,在寄存器中改值,3写,从寄存器重新写入内存。因此在多线程中,对全局变量的操作,都是不安全的。
初值i=0; 那么i++在两个线程中分别执行100次,能得到的最大值和最小值分别是多少?(2-200)
C++ 11的原子变量: atomic头文件,std::atomic<int> i = 1;
12. 如何限制对象只能建立在堆上 或者 栈上
1. 对象只能在堆上建立,就是不能声明局部变量,只能通过new创建对象
将析构函数设置为私有,对象就无法创建在栈上了
2. 对象只能在栈上建立
禁用类的new运算符,重载operator new() 为私有,并且要重载delete
13. 线程安全的单例模式
分为懒汉模式和饿汉模式
1. 饿汉模式:
template<class T>
class singleton
{
protected:
singleton(){}
private:
singleton(const singleton&){} //禁止拷贝
singleton& operator=(const singleton&) //禁止赋值
static T* m_instance;
public:
static T* getInstance();
}
template<class T>
T* singleton<T> :: getInstance(){
return m_instance;
}
template<class T>
T* singleton<T> :: m_instance = new T();
在还未使用变量时,已经对m_instance进行初始化赋值了,不存在多线程实例化问题
2. 懒汉模式
template<class T>
T* singleton<T>::getInstance(){
if(m_instance == NULL){
m_instance = new T();
}
return m_instance;
}
初始化时m_instance先为空,在调用获取实例的方法时,判断是否需要创建对象,所以这种需要加锁:
template<class T>
T* singleton<T>::getInstance(){
pthread_mutex_lock(&m_mutex);
if(m_instance == NULL){
m_instance = new T();
}
pthread_mutex_unlock(&m_mutex)
return m_instance;
}
每次进来都需要加锁,影响效率,因此有了 “双检锁机制”
template<class T>
T* singleton<T>::getInstance(){
if(m_instance == NULL){
pthread_mutex_lock(&m_mutex);
if(m_instance == NULL){
m_instance = new T();
}
pthread_mutex_unlock(&m_mutex)
}
return m_instance;
}
又有可能调用new T()时,初始化还没有完成m_instance已经有值了,导致另一个线程获取到还没有初始化完成的指针,添加一个临时变量即可
T* temp = new T();
m_instance = temp;
linux中还有一种实现,
static pthread_once m_once;
void singleton<T>::Init(){
m_instance = new T();
}
T* singleton<T>::getInstance(){
pthread_once(&m_once, Init);
return m_instance;
}
pthread_once_t singleton<T>::m_once = PTHREAD_ONCE_INIT;
14. fread和read fopen和open fwrite和write的区别
1. fread是C标准库函数,read是系统调用
2. fread带有缓冲,是通过read实现的,read不带缓冲,可以直接访问硬件
3. fread返回一个FILE结构指针,read返回的是int文件号
4. 如果文件大小时8K,read和write只分配了2K缓存,则需要调用4次系统调用来从磁盘上实际读写,如果用fread和fwrite,系统会自动分配缓存,读取文件只需要一次调用
5. 处理文件用fread,处理套接字,管道等,用read
15. c++11中的智能指针
1. 智能指针的作用
1. 智能指针实质是一个对象,行为表现像一个指针
2. 防止未调用delete和程序异常进入catch忘记释放内存,多次释放同一个指针等
3. 把值语义转为引用语义
2. 智能指针的使用,在c++11之后,<memory>头文件中,是一个模板类
1. shared_ptr,多个指针指向同一个对象,使用引用计数,每一个shared_ptr的拷贝都指向同一块内存,每使用他一次,内部引用计数加1,每析构一次,内部引用计数减1,减为0时,自动释放堆内存,shared_ptr内部引用计数是线程安全的,但是对象读取需要加锁。
int a;
std::shared_ptr<int> ptra = std::make_shared<int>(a);
std::shared_ptr<int> ptrb(ptra);
cout << ptra.use_count() << endl;
int* pb = ptrb.get() //获取原始指针
注意不要用一个原始指针初始化多个shared_ptr,会造成重复释放
要避免循环引用
2. unique_ptr同一时刻,只能有一个unique_ptr指向给定的对象
std::unique_ptr<int> uptr(new int(10)); 绑定动态对象
std::unique_ptr<int> uptr2 = uptr 错误,不能赋值,不能拷贝
std::unique_ptr<int> uptr2 = std::move(uptr) 转移所有权
uptr2.release() 释放所有权
超过作用域,自动释放内存
3. weak_ptr,为了配合shared_ptr,像旁观者一样,它的构造不会引起指针引用计数增加。
std::shared_ptr<int> sptr = std::make_shared<int>(10);
std::weak_ptr<int> wp<sptr>;
wp.expired() 表示引用计数
std::shared_ptr<int> sptr2 = wp.lock(); 获取一个可用的shared_ptr对象
weak_ptr 可以解决循环引用的问题