二维数组有两种存储方式
1,行优先存储 在连续的内存上,一行一行的安在上面
2,列优先存储 在连续的内存上,一列一列的安在上面
c/c++数组都是采用的行优先存储
例题: 在c语言中有一个矩阵大小16K*16K,若对两个这样的矩阵进行运算,行优先读取与列优先读取的区别?
A 一样快 B,行优先快 C 列优先快 D 无法确定
答案:B
解析:因为c/c++是行优先存储,那么我们采用行优先读取的话,从内存中取数据就可以按顺序遍历取出来,但是列优先存储还有四处移动指针找内存位置
二维数组的存储方式其实是和一维数组是一样的
但是如果使用的是双重只能指向的二维数组就不能用一维读取方式,因为访问的会是地址
指针+/-操作移动数不直接是字节数,而是 当时类型的字节数*移动数 个字节,因为这样比较人性化
指针和指针的操作只限于指向同一个数组,那求得就是两个之间的距离
const修饰指针
1,const int *p
代表 *p不可以变,即p指向的那个变量的内容不可以变
2,int * const p
代表 p不可以变,那么说明p的指向不能变
3,const int * const p
代表p的指向不可以变,p指向的变量的内存也不能变,相当于想通过p指针来操作那个变量的话,只能访问值,变成了常量
class 和 struct 的区别
class默认是 private权限 , struct 默认是 public权限,除此之外没得区别
结构体位域
里面变量使用方法和结构体变量没什么区别,只是可以开始定义的时候就指明该变量只用内存多少位
例子:struct{
int a:1;
int b:3;
int c:4;
}
这样的话a 只占空间1位,最大值可以到1 其他两个类似,位域主要目的就是用于压缩空间
大小端
大端:高字节存储在低地址上
小端:高字节存储在高地址上
低地址: 内存中是由地址编号的,地址小的就是低地址
低字节: 0x12345678 低字节应该是78 这个很人性化,你的78是十位和个位肯定是存储在低的字节
例题: 假设在一个32位的环境下,CPU是小端模式,所有参数用栈传递,则执行一下程序结果多少?
int main(){
long long a=1,b=2,c=3;
printf("%d %d %d",a,b,c);
}
A,1 2 3 B,1 0 2 C,3 2 1 D,3 2 0
答案:B
解析:因为是小端系统,然后又是栈传递,printf是栈传递
存储情况:1 0 2 0 3 0,long long 是8个字节,int是4个字节,所以我们输出前面3个4字节,所以 1 0 2
例题:32位小端字节序的机器上如下代码
char array[12]={0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08};
short *pshort=(short *) array
int *pint = (int*) array;
long long *pint64=(long long *) array;
printf("0x%x 0x%x 0x%x 0x%x",*pshort,*(pshort+2),*pint64,*(pint+2));
A 0x201 0x403 0x8070605004030201 0x0
B 0x201 0x605 0x8070605004030201 0x0
C 0x201 0x605 0x4030201 0x8070605
D 0x102 0x506 0x102030105060708 0x0
E 0x102 0x304 0x1020304 0x5060708
F 0x201 0x605 0x4030201 0x6050403
答案:B
struct的内存大小计算
sizeof的使用
struct 内存大小遵循两个原则
1,肯定是最大的成员内存的最小整数倍,如果>4 那么就当是4的倍数即可
2,内存对齐原则,按成员的顺序来分配内存,如果分配到当前的成员的时候,我们必须是当前最大成员的倍数,如果超过8的话是4的倍数即可,但是当时的那个必须是最大的倍数
struct s1{
char a;
double b;
int c;
char d;
}
struct s2{
char a;
char b;
int c;
double d;
}
这两个结构体的内存空间分别是 24 和 16
s1:因为 a是1然后是1 -> 因为b是8,要是8的倍数,当前9最近的是16,然后就变成了16 -> 遇到int的时候就变成 20-> 遇到 d 变成 24
s2:a是1->b是2->c变成 8 ->d变成 d
所以我们大字节的变量尽量放在后面定义能够节省内存空间
如果结构体里面有结构体的话,那么我们不能把结构体当作一个变量来处理,而是相当于把里面这个结构体给拆开,把里面的变量都取出来来进行前面的两个规则
#pragma pack ()
只有两种操作
#pragma pack(n) 代表以 1 2 4 8 16.....这些字节对齐
#pragma pack() 取消自定义的字节对齐
计算的时候取类型字节数和对齐字节数的最小值
const 和 #define的区别
主要从define只是用来替换来拓展的
1,define不占用内存 ,const 实际上是个变量,所以需要内存
2,define 是在编译的时候就已经替换好了,const是在运行的时候
3,define 作用域不受函数影响,const实际上是个变量会有影响
4,define 不能用来传参,const因为是个变量所以可以传参
5,define 因为只是替换,不会进行语法类型检查就只接替换掉了,const因为是变量会进行语法检查
编码风格
char a='A';
if('A'==a) 与 if(a=='A') 哪种好
答案:第一种
解析:因为如果是==错写成=的话第一种能直接报错
static作用
1,隐藏
如果定义成全局的话,相当于定义成了全局访问区,其他的源文件也能够访问到,加上static就能只是当前文件能够访问而已
作用:能在不同的源文件里面重名的定义变量
2,给局部变量持久化
作用:根据不同业务的需求决定使用
3,初始化为0
const 必须要初始化定义值,因为常量不可修改,所以在类中如果有const成员时候必须在构造函数里面初始化const
内存管理
分为 堆 ,栈,全局存储,文字常量,代码 五个区
1,堆和栈的区别
解析:堆就是用new和 malloc等申请来的内存,必须要用户自己去释放
栈:定义的变量之类的,过了生命周期自动销毁
例题:下面程序的输出结果是()
void getmemory(char *p){
p=(char *)malloc(11);
}
int main(){
char *str="hello";
getmemory(str);
strcpy(str,"hello world");
printf("%s",str);
return 0;
}
A hello B hello world C hello worl D runtime error/Core dump
答案:D
解析:因为str指针传参过去,p和str开始都是指向常量区的hello ,传参只能说明可以代替修改值,不能改变指向,p指向另一个存储空间的时候,说明p和str已经指的不是一个东西了
后面str再去赋值,常量区没有给他内存很明显不可以就出错了
内存碎片
操作系统方面
分为外碎片和内碎片
外碎片:已经把内存空间分配出去了,但是因为内存空间是等大小的,而申请内存的对象只是用到一部分,其他的相当于浪费了
内碎片:因为内存是连续的,如果 1 2 3 4 5 6 7 , 来了两个对象分别申请了3,4 和 6 7 ,要是再来一个申请3,内存空间是不足以分配的,也就是剩下的内存太小导致无法利用
解决方案:段页式存储
段分配实现了可以不连续存储,页因为里面单位本来就不大也就避免了外碎片
如何在频繁的new和malloc下减少内存碎片和提高性能
内存池:内存池的作用就是减少内存碎片和提高效率,通过申请一批内存大小一样的内存块(相当于缓存),一次性申请这么多,然后后面就不用每次去使用内存分配函数了
解析:因为new 和malloc 为了通用所有情况,分别在多线程时候加了锁,还有在找大内存块的时候如果比需要的内存大还要进行分割,这样又会留下内存碎片
如果频繁使用内存申请,那么这些步骤都要进行,所以导致了速度较慢,内部实现的通用的内存池在某些特定的业务上比不上特定的内存池,所以就要我们自己进行编写
先申请了一批内存,内存池分成很多块,一块分为很多个节点,都是用链表模拟实现出来的,这样我们分配内存的话就只用在这个内存池上分配,
自己写的单线程池的话就避免了多线程锁的时间花费,然后也因为我是一次性分配的这块内存,这样就比去申请多次遗漏的内存碎片少
!!!有时间自己一定要手写内存池
啥是内存泄漏?
内存泄漏也就是说这块内存给浪费掉不能再使用了,c++里面是指new ,malloc等在堆里面申请内存,然后必须显示使用 delete free 来释放内存,如果没有释放掉
等程序结束,内存没有释放掉,而且也没有办法找到来释放,这样就造成了内存泄漏,多运行几遍程序内存就会泄漏完,后面无法再申请空间,只能重启才可以
缓冲区溢出
是指指向缓冲区内的数据大于缓冲区的容量,那么就要就会造成缓冲区溢出(就和数组存不下长度,runtime error差不多)、
内联函数
和宏的功能差不多,目的是为了减少调用函数的开销,表面是一个函数,但是在编译的时候会把函数那个位置自动替换成执行代码,一般只适用于短代码函数
默认参数
定义如果没有值传进来的话,默认使用这个值 f(int a,int b=1,int c=2) 定义的时候必须把默认参数放在后面,所有普通参数要在默认参数之前,调用的时候一样的也要
使用普通参数一定要在使用默认参数之前
类的单继承例题
void Test(){
class B{
public:
B(){
cout<<"B"<<"\n";
}
~B(){
cout<<"~B"<<"\n";
}
};
class C{
public:
C(){
cout<<"C"<<"\n";
}
~C(){
cout<<"~C"<<"\n";
}
};
struct D:B{
D(){
cout<<"D"<<"\n";
}
~D(){
cout<<"~D"<<"\n";
}
};
private:
C c;
}
输出 B C D ~D ~C ~B
解析:因为D继承了B所以先调用了B的构造函数,然后因为创造了C对象,所以输出C,最后轮到D,然后析构的话就是和构造 相反的方向
拷贝构造函数和重载赋值运算符区别
拷贝构造函数用于初始化对象 ,返回一个对象,(所有需要创建对象的地方,所以带了构造函数四个字
赋值运算符严格来说还是赋值,所有用到了赋值的地方都要;
重载赋值运算符的有没有返回值也影响到了能不能连续赋值 a=b=c
公有继承,保护继承,私有继承
三个继承都遵循一个道理:私有成员无论是哪个都不能被继承,本身private成员就是只是在当前类中使用,然后 根据自己的继承方式 继承来的权限是 min(基类权限,继承方式的权限) public最高
public成员 protected成员 private成员
public 继承 public protected 无法继承
protected 继承 protected protected 无法继承
private 继承 private private 无法继承
多继承时发生的二义性
1,多继承的时候,会有多个this指针,分别指向自己继承的子对象,然后地址是按连续存储的
#include<bits/stdc++.h>
using namespace std;
class Base1{
public:
char s;
print1(){
cout<<"Base1="<<this<<"\n";
}
};
class Base2{
public:
char s;
print2(){
cout<<"Base2="<<this<<"\n";
}
};
class temp1{
public:
char s;
print(){
cout<<"temp1="<<this<<"\n";
}
};
class temp2{
public:
char s;
print(){
cout<<"temp2="<<this<<"\n";
}
};
class dan:public Base1,public Base2{
public:
temp1 m1;
temp2 m2;
void print(){
cout<<"dan="<<this<<"\n";
print1();
print2();
m1.print();
m2.print();
}
};
int main(){
//int x;
dan *x = new dan();
x->print();
delete x;
}
多继承本来是把继承而来的直接替换掉在本类中,但是还是会存在多个this指针
2,如果多继承的类中存在相同的变量名和函数名
这里编译器就会不知道调用哪一个,这里就要加上类名强调是哪一个的 A::print
最好的方法是利用隐藏在子类中写一个要用的方法
3.菱形继承
如果产生下面这种情况
基类是base,d1和d2分别都继承于base,又创建对象mi同时继承于d1和d2,这样d1和d2里面的base对象也都被它继承了,相同变量函数我们想到加类名那么这个其实也是一样的
子类到基类 ->上行转换
基类到子类 ->下行转换
分别可以使用类强调是属于哪一个的
下行转换的时候我们只要在前面加上类名强调即可
上行转换中间历经两个类,有两条路选择,编译器不知道选择哪一条, 那我们就帮他做了这一步的事情,先强转到B再到A
虚函数多态
首先分为动态绑定和静态绑定
动态绑定:必须是经过virtual修饰的,动态绑定发生在运行阶段,不受自身类型的限制,指向什么类型就是什么类型
静态绑定:静态绑定发生在编译阶段,受自身类型的限制,利用子类父类的转换,转换后使用的是自身类型的东西
c++里面默认不使用动态绑定,要使用动态绑定要触发两个条件
1,被修饰的函数必须是虚函数
2,通过基类的指针或者引用去操作
虚函数表指针和虚基类表指针
虚函数表指针
首先我们要懂内存中存储是怎么样的
类里面有成员变量和成员函数
成员变量分为:static 普通变量
成员函数分为:static 普通函数 virtual函数
变量:static属于类不属于对象,普通变量属于对象
函数:static函数,普通函数属于类,virtual函数属于类中创建了一个虚函数表,只是每个对象会创建一个虚函数表指针vptr,然后通过指针操作虚函数
答案:BCD
解析:因为aptr只是一个没有创建出对象,并没有对象的一些东西,
B:f2虽然属于类,但是里面操作了成员变量所以错
C:虚函数,必须要有对象里面的虚函数指针才能操作
D:综合了BC
sizeof(A)和sizeof(B)分别是多少?
1 和 4
解析:因为空类的时候,为了不让不同的对象占用相同的地址,所以特别的用了 1byte做区别, B类的话有一个虚函数指针
说明自身的虚函数表指针的数量取决于继承的父类的个数
虚基类表指针
核心观念:无论派生多少层,每个实体对应只实现一次
这个可以用之前的菱形继承做比喻
A是基类,B,C分别继承A,然后D继承B,C
如果按普通继承的话,那么D里面会有两个对应的A子对象,构造两次
但是如果是采用virtual继承的话,那么A对象只会构造一次
纯虚函数
纯虚函数也就是里面有一个虚函数并没有实现,只是给了一个名称,目的就是留给子类去实现
有纯虚函数的类也叫抽象类,无法实例化对像(原因是因为这个函数都不能使用,所以实例化肯定有风险,禁止
如果子类没有把父类抽象类的所有纯虚函数实现出来,那么也是抽象类,因为子类继承了父类所有的函数
答案:B
解析:因为纯虚函数也只能有虚函数指针才能使用到,属于对象才能使用,
四种显示强制转换和typeid
typeid 可以返回当前对象的类型是什么,
具体用法:
int *a,*b;
if(typeid(a)==typeid(b)){
printf("YES");
}
四种强制转换
dynamic_cast static_cast const_cast reinterpret_cast
reintrepret_case的用法同我们之前普通用括号是一样的
//第一种普通用括号
char x;
int y;
y=(int) x;
//第二种用reintrepret_cast
char x;
int y;
y=reintrepret_cast(int)x;
这两种的效果是一样的
const_cast
作用:增加或者删除类型的const属性都是用这个,这个而且只能用于增删const属性,不然会编译错误
static_const
作用;类型之间存在隐式类型转换可以进行强转
两个用途
1.基本类型之间,基本类型之间存在隐式转换关系,基本类型指针和引用不存在隐式转换关系,所以不可以使用static_cast转换,但是可以用void*做第三者在中间做媒介
2.有继承关系的下行转换(指针,引用)
下行转换:基类转换成子类(没有动态类型检查,是不安全的)
dynamic_cast
作用:主要用于类有继承关系的时候进行的转换(只能用于有虚函数的类,因为这个转换会执行动态类型检查,但是动态类型这个东西只存在于虚函数表内,如果没有虚函数使用就会编译错误)
dynamic_cast主要用于上行转换和下行转换
上行转换的时候:dynamic_cast 和 static_cast的效果是一样的
下行转换的时候: dynamic_cast 比 static_cast 更安全
总结:不要用 static_cast 这个不得行,用dynamic_cast
上行转换 : 两个差不多
下行转换:dynamic_cast发现错误更早,更安全
兄弟间转换:static_cast 要做到这个操作还要分两步,要有第三者的插足,太麻烦
!!!严格记住 虚函数的多态操作只和指针和引用之间有关,任何对象的直接赋值上下行转换都是属于静态绑定和普通的赋值无异
析构函数弄成是虚函数的好处体现在哪里?
其实c++的有一条规则,如果子类调用构造函数,那么还会调用父类的构造函数
子类调用析构函数,那么父类也会调用析构函数
然后因为多态的存在我们时常会出现以下情况
class B
{
public:
B() { printf("B()\n"); }
~B() { printf("~B()\n"); }
private:
int m_b;
};
class D : public B
{
public:
D() { printf("D()\n"); }
~D() { printf("~D()\n"); }
private:
int m_d;
};
B* pB = new D();
如果经过多态后,指向的是B类,但是析构的时候并不能把另一块在D上的内存释放掉,就会造成内存泄漏
如果虚函数析构的话,那么我们实际上调用的是 ~D 子类的析构函数,而且会因为c++的特性调用父类的析构,然后达到完美释放掉内存
构造函数为什么不能是虚函数?
因为没有意义,还有虚函数如果要调用的话,要用虚函数里面的虚函数表指针,但是虚函数表指针存在于具体对象中,如果构造函数就是虚的,那么无法创建。变成了矛盾的情况