1 7种设计原则
① 单一职责原则(SRP):类的指责单一只干一件事;
② 开闭原则(OCP):可扩展不可修改(在不修改源码的情况下,扩展你的功能);(工厂)
③ 里氏代换原则(LSP):一个系统中能接受基类对象的地方必能接收子类对象;
④ 依赖倒转原则(DIP):要针对抽象层编程,而不要针对具体类编程;游戏壳
⑤ 接口隔离原则(ISP):使用多个专门的接口来代替一个系统的接口;(多类聚低耦合)
⑥ 合成复用原则(CRP):多用组合、聚合、关联,少用继承(避免多继承混乱);
⑦ 迪米特原则(LOD):少用引用,如果两个类不是必须彼此通信,那就不让他俩有直接关系,通过第三者间接交互。(最少知道)
2 类之间关系
类间关系有很多种,在大的类别上可以分为两种:纵向关系、横向关系。
㈠ 纵向关系
继承:有好多类,相同的部分放到基类,不同的不部分放到子类,子类继承父类,就可以获取父类的功能,并且可以扩展自己的功能。
㈡ 横向关系
较为微妙,按照UML的建议大体上可以分为四种:
它们的强弱关系是没有异议的:依赖 < 关联 < 聚合 <组合
⑴ 无生命期关系
① 关联:* 某个对象会长期的持有另一个对象的引用,而二者的关联往往也是相互的。关联的两个对象彼此间没有任何强制性的约束,只要二者同意,可以随时解除关系或是进行关联。(人和朋友)(实线箭头)
② 依赖:&就是对对象的一种临时需要,某个对象的功能依赖于另外的某个对象,而被依赖的对象只是作为一种工具在使用,而并不持有对它的引用。(人呼吸空气)(虚线箭头)
⑵ 有生命期关系
③ 聚合:(弱关联)聚合是强版本的关联。它暗含着一种所属关系以及生命期关系。被聚合的对象还可以再被别的对象关联,所以被聚合对象是可以共享的。虽然是共享的,聚合代表的是一种更亲密的关系。(家和成员)(空心菱形实线箭头)
④ 组合:(强关联)组合关系就是整体与部分的关系,部分属于整体,整体不存在,部分一定不存在,然而部分不存在整体是可以存在的,说的更明确一些就是部分必须创生于整体创生之后,而销毁于整体销毁之前。(人和脑袋)(实心菱形实线箭头)
3 设计模式
设计模式只是一种设计思想,针对不同的业务场景,用不同的方式去设计代码结构,其最最本质的目的是为了解耦,延伸一点的话,还有为了可扩展性和健壮性,但是这都是建立在解耦的基础之上。
⑴ 单例模式:
使用环境:不能在外面定义类的对象(构造和析构都私有化)
目的:保证一个类仅有一个实例,并提供一个访问他的全局访问点,该实例被所有程序模块共享。
注意:①类中构造和析构函数必须是私有或者受保护的,确保在别的地方不能创建该类的实例;
②定义一个私有的静态指针,指向该唯一的实例;
③实例函数是返回值为指针的静态函数。
单例模式(类不能在外面定义对象,构造私有或者受保护)
因为在UDP中要定义CMyWnd的对象,去注册选择事件,而CMyWnd的构造是受保护的,不能在外面直接使用,因此采用单利模式,定义一个静态的CMyWnd的指针,用于接new出来的CMyWnd的对象,new的过程是在类内部写一个静态的接口函数,当,CMyWnd指针为空的时候接new出来一个CMyWnd对象,,并作为返回值返回,从而得到该对象,接下来用该对象进行一系列的调用。静态指针类外初始化。
⑵ 工厂模式
需要什么就会生产出什么。
一般分为3种。简单工厂,工厂和抽象工厂。
①简单工厂:给它传进去一个类型,工厂就会把对应的实体创建出来,通过switch(case) new car bus smallcar等,如果有来一种车,就得添加一个case,原代码要不断修改,违背了开闭原则(可扩展不可修改)
项目中:UI界面点击“发送”,通过UDP还是TCP通信,要通过控制类(中介者决定),只需中介者里放一个指针(父类INet*),在构造函数里运用简单工厂模式,给UDP发,就new出一个UDP对象,TCP就new出TCP的对象,具体的功能只需要用自己的指针调用对应UDP或者TCP的具体实现函数即可。
② 工厂
③ 抽象工厂:
定义一个纯虚函数,通过每个类继承父类,在自己的类中是实现自己实体的创建,这样就不需要更改原代码,解决了简单工厂违反开闭原则的问题,但是每需要创建一种类型,都需要写一个类去继承,从而造成接口会比较多。
④工厂模式中,一个工厂生产一个产品,所有产品派生于同一个抽象产品(或产品接口);而抽象工厂模式中,一个工厂生产多个产品,它们是一个产品族,不同的产品族的产品派生于不同的抽象产品(或产品接口)。
⑤关键点如下:
一、三种工厂的实现是越来越复杂的
二、简单工厂通过构造时传入的标识来生产产品,不同产品都在同一个工厂中生产,这种判断会随着产品的增加而增加,给扩展和维护带来麻烦
三、工厂模式无法解决产品族和产品等级结构的问题
四、抽象工厂模式中,一个工厂生产多个产品,它们是一个产品族,不同的产品族的产品派生于不同的抽象产品(或产品接口)。
⑶中介者模式
项目中:UI界面点击“发送”,通过UDP还是TCP通信,要通过控制类(中介者决定),只需中介者里放一个指针(父类INet*),在构造函数里运用简单工厂模式,给UDP发,就new出一个UDP对象,TCP就new出TCP的对象,具体的功能只需要用自己的指针调用对应UDP或者TCP的具体实现函数即可。
①什么是中介这模式,中介者模式是干什么的?
小A、小B、小C和小D相互说话,因为没有任何途径,所以只能当面说或者让别人传达,假设小B和小A说话的同时小C呼叫小A,小D呼叫小B,那么小A和小B就会不知道是要听还是要回话,就会处于一种过度耦合的状态,如果人数扩展到几百人,就会形成一个庞大的网状对象之间过度耦合的状态,解决这种过度耦合的状态,就用到了中介者模式,中介者模式就是把网状的对象之间过度耦合的状态改为星形的结构,
这样如果小A和小B说话,小A只需要拿起手机就可以和小B说话,而这个手机就是中介者身份,从而降低了对象之间的耦合状态。
②适用场景
1.一组对象,进行复杂的通信;
2.想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。
③优缺点
优点:简化了对象之间的关系,系统变为松耦合,使各个同事对象独立而易于复用。
缺点:1 中介者角色承担了较多的责任,一旦中介者出现了问题,整个系统都会受到影响;
2 如果新增加一个同事类是,要修改抽象中介者类和具体中介者类,这时可以用观察者模式来解决这个问题。
④总结
中介者模式,定义了一个中介对象来封装系列对象之间的交互;中介者使各个对象不需要显式的相互引用,从而使其耦合性降低,而且可以独立地改变他们之间的交互。
⑷生产者/消费者
一组生产者进程和一组消费者进程共享一个初始为空大小为n的缓冲区,只有缓冲区没满时,生产者才能把消息放入到缓冲区,否则必须等待;只有缓冲区不空时,消费者才可以从缓冲区取消息,否则也必须等待;因为生产者之间是互斥的,所以缓冲区为临界资源,它只允许一个生产者放入消息,或者一个消费者从中取消息。
例如:桌子上有一只盘子,每次只能向其中放入一个水果。爸爸专向盘子中放苹果,妈妈专向盘子中放橘子,儿子专等吃盘子中的橘子,女儿专等吃盘子中的苹果。只有盘子为空时,爸爸或妈妈就可向盘子中放一个水果;仅当盘子中有自己需要的水果时,儿子或女儿
可以从盘子中取出。
问题分析:
1) 关系分析。这里的关系稍复杂一些,首先由每次只能向其中放入一只水果可知爸爸和妈妈是互斥关系。爸爸和女儿、妈妈和儿子是同步关系,而且这两对进程必须连起来,儿子和女儿之间没有互斥和同步关系,因为他们是选择条件执行,不可能并发,如图2-8所示。
2) 整理思路。这里有4个进程,实际上可以抽象为两个生产者和两个消费者被连接到大小为1的缓冲区上。
3) 信号量设置。首先设置信号量plate为互斥信号量,表示是否允许向盘子放入水果,初值为1,表示允许放入,且只允许放入一个。信号量 apple表示盘子中是否有苹果,初值为0,表示盘子为空,不许取,若apple=l可以取。信号量orange表示盘子中是否有橘子,初值为0,表示盘子为空,不许取,若orange=l可以取。
///C++知识点/
1 类:具有相同属性和行为的对象的抽象。行为就是方法。
2 对象:
类的实例(用类定义出来的变量,类的对象)。具有属性和行为。对象的一些行为会改变自身的状态,对象是惟一的,他的惟一性来源于内存地址的惟一。
3 类能否被实例化?单例(只能定义一个实例)
1 类中有什么?
访问属性(public,protected,private(默认访问属性))等。
类的继承方式:基类子类的访问权限。
2 C++空类里有什么?六个函数,一个this指针,
空类大小是1,用于占位。
构造:与类同名,没有返回值,可以有参数;作用是初始化,如果构造不初始化,可以在初始化列表中初始化(顺序是定义的顺序);不可以被继承;
析构:与类同名,无参数无返回值,且一个类只有一个析构;作用:对象生命周期结束时回收空间;调用delete是执行两个动作(调用析构,回收空间),虚析构。
拷贝构造:浅拷贝,深拷贝(涉及内存空间开辟的);什么时候调用拷贝构造?对象作为参数值传递时,对象当作返回值以值传递方式返回时,一个对象初始化另外一个对象时。拷贝构造和赋值运算符区别?拷贝构造源对象存在而目标对象未被创建,赋值运算符源对象目标对象都存在。
赋值运算符=:返回值是一个引用(好处是可以连等)
取地址符&:
Const取地址符const &,前四个要会重写),
This指针:this指针里装着当前类实例的地址;是隐式调用;可以区分局部函数和全局函数(类外函数);类成员函数(静态成员函数)内没有this指针。
6 类和struct的区别?
C++中访问属性不同,类是私有的,struct是公有的。
7 C++结构体和C语言里struct区别?
8 C++的三个特性:封装,继承,多态
封装:可以隐藏实现细节,是代码模块化。(eg,C++直接调用strcpy是因为该函数已 经封装在string类里面了);
继承: 分为单继承和多继承,可以扩展已存在的代码模块,子类一旦继承父类,他就拥有父类的所有东西,并且可以扩展自己的功能;
多态:多态性体现在运行和编译两个方面,
编译期多态又叫静态多态,是通过运算符重载,函数重载,模版实现,
运行期多态又叫动态多态,是通过虚函数实现。
①空类的大小是1,用于占位;
②虚函数虚指针
有虚函数就会有虚函数指针,指向一个虚表,虚表本身是一个函数指针数组,存放当前虚函数入口地址;虚函数和普通函数的区别?普通函数的名字就是地址,虚函数不是。
③那些函数不能被声明成虚函数?
构造函数(虚函数是对对象操作的,先有构造,虚函数才能对对象进行绑定),
内联函数((因为内敛函数跟虚函数不再一个期,内敛(编译期),虚函数(运行期);内敛函数编译期直接展开省去函数跳转,分为隐式内敛(代码短小,前面不加inline),显示内敛(函数前加inline,但是代码不能长);优点:),
静态成员函数(因为静态成员函数本来就是共享给该类的所有实例,因此不需要);
友元函数(C++不支持友元函数的继承,友元函数是为类外函数提供访问类中所有成员的方法);
非成员函数。
9 纯虚函数=0,
包含纯虚函数的类称为抽象类。如果一个类里所有函数都是纯虚函数,称为接口类。抽象类不能实例化(所以抽象类不能做函数参数和返回值),所有继承抽象类的子类都必须实现抽象类的所有方法。抽象类可以定义指针和引用。
10 引用
(两个及以上变量指向同一块地址空间)给变量起别名,不占内存空间;必须被初始化;一旦被初始化不能被重新引用;没有指向空值的引用。
11 构造方法用来初始化类对象,与父类的其他成员不同,他不能被子类继承,因此,在创建子类对象时,为了初始化从父类继承来的数据成员,系统需要调用父类的构造方法。
继承时的构造原则:
①如果子类没有定义构造方法,则调用父类的无参构造;
②如果子类定义了构造方法,无论是无参还是带参,在创建子类对象的时候,首先执行父类无参的构造方法,然后执行自己的构造方法;
③在创建子类对象时,如果子类的构造函数没有显式调用父类的构造函数,则会调用父类默认无参构造函数;
④在创建子类对象时,如果子类的构造函数没有显式调用父类的构造函数,且父类自己提供了无参构造函数,则会调用父类自己的无参构造函数;
⑤在创建子类对象时,如果子类的构造函数没有显式调用父类的构造函数,且父类只定义了自己的有参构造函数,则会出错(如果父类只有有参数的构造方法,则子类必须显式调用此带参方法)
⑥如果子类调用父类带参数的构造方法,需要用初始化父类成员对象的方式。
12 内存分区
堆栈是程序运行时由系统分配,剩下三个段是编译时由编译器分配。
栈区: 用于存放程序临时创建的局部变量,即{}中定义的变量,每个进程都有自己的栈空间
堆区: 用于存放进程运行中被动态分配的内存段,大小不固定可以通过malloc,free等动态扩张和缩减
BSS段: 未初始化的全局和静态变量
数据段: 已经初始化的全局或静态变量
代码段: 程序执行的代码
13 函数指针
(用来提高代码的扩展性,以不变的代码实现可变的算法)
Typedef void(*FUN)() 函数指针类型是void (*)()
别名是FUN
调用时:FUN pfn = &Show;
(*pfn)();
Void (*pfn1)(); 定义一个函数指针的变量,变量名为pfn1
14 const
Const变量定义必须初始化
Const+指针的意义
Const放函数后面:表示该函数是常成员函数,可以理解为“只读”
前面:表示返回值是常量
指针常量:int*const p
常量指针:int const*p
指针数组:int *p[5]
数组指针:int(*p)[5]
15 函数的参数有三种:值,址,引用
如果要改变传进来的东西:址,引用
如果要不改变传进来的东西:值
16 new/malloc区别?
①malloc/free是C标准库函数,不可以重载,new/delete是运算符可以重载,两者都是动态申请和释放内存;
②malloc从堆上动态分配内存,new从自由存储区上为对象分配内存;
③new过程分为三步(1,调用operator new 分配空间2,调用构造函数构造出对象3,返回指向该对象的指针;如果是new[]如果编译器看到[]会自动向前走4个字节用于计数,当删除的时候编译器看到[]知道开辟的是一个数组,会向前走4个字节开始释放空间),delete过程分为两步(1,调用析构函数,释放对象2, 调用operator delete函数释放空间),malloc和free不可以为对象分配内存,它不会调用构造和析构函数;
④malloc返回值是void*需要强制转换成我们需要的类型,new操作符内存分配成功时,返回对象类型的指针,不需要强制转换;
⑤malloc分配内存失败返回NULL,new分配内存失败会抛出异常;
⑥malloc申请空间需要指定内存块大小,new不需要指定内存块大小。
17 IO模型
进程运行状态分为内核态和用户态两种。
对于文件读取:
第一步:等待数据准备
第二步:将数据从内核空间复制到用户空间中
对于socket:
第一步:等待网络上的数据到达,然后被复制到内核空间
第二步:将数据从内核空间复制到用户空间中
如果有多个任务要执行,这些任务必须逐个而不能并发地执行,其他任务等待当前任务完成后才执行。
如果有多个任务要执行,这些任务可以并发地执行而不用互相等待。
任务执行过程中,如果没有马上获得调用结果,那么线程被挂起直到有调用结果后返回。
任务执行过程中,如果没有马上获得调用结果,那么线程立即返回。
同步和异步针对的是任务之间能否并发执行,阻塞和非阻塞针对的是任务执行过程中线程的状态。
recvfrom会一直阻塞,如果有多个socket都想接收数据,而只有一个接收缓冲区,因此线程池也解决不了,解决办法:IO模型
⑴同步阻塞IO
进程执行recvfrom系统调用后进程阻塞,等待数据准备好,此时进程让出CPU。当数据准备好后,等待内核将数据复制到用户空间。复制完成后,recvfrom系统调用返回成功,进程解除阻塞。此模型特点是IO过程的两步都会等待。
⑵同步非阻塞IO
进程执行recvfrom系统调用,如果数据还没有准备好,那么recvfrom系统调用返回一个错误。这个过程一直重复,直到数据准备好后,等待内核将数据复制到用户空间。复制完成后,recvfrom系统调用返回成功。此模型特点是,IO过程的第一步不需要等待,而是用户进程需要不断地询问内核数据是否准备好,此时进程不会让出CPU,而会一直占用CPU,浪费了大量的CPU资源,因此不常用。
⑶IO多路复用select模型(同步 )
进程执行select系统调用后,进程阻塞,内核一直轮询文件描述符数组中每个socket状态,当某个套接字数据准备好了,则select系统调用返回。此时进程发起recvfrom系统调用,内核将数据复制到用户空间。复制完成后,recvfrom系统调用返回成功。
此模型特点是,对于单个IO操作,和阻塞IO相比并没有什么不同。事实上,还更差一些。因为这里需要使用两个系统调用(select 和 recvfrom),而阻塞IO只调用了一个system call (recvfrom)。不过它适合于同时处理多个IO操作,当其中的任意一个进入可读状态,select系统调用就可以返回。
在非阻塞IO中,不断地询问socket状态是通过用户进程去进行的,而在多路复用IO中,轮询每个socket状态是内核在进行的,这个效率要比用户进程高的多。
///select(变量,线程)///
//4 绑定成功 创建线程--接收数据 recvfrom() 线程实现函数必须是静态的类成员函数
//m_bFlag = TRUE; //while循环结束条件-------------------------------------------------------------------------------------
//m_hThreadRecv = (HANDLE)_beginthreadex(NULL,0,&ThreadRecv,this,0,NULL);//安全属性,栈的大小,函数地址,参数列表,执行标志,线程ID
//if(0 >= m_hThreadRecv)
//{
// UnInitNetWork();
// returnfalse;
//}
//1 定义一个数组
//2 将set初始化成空集合(数组清空)
//3 将套接字加入到集合(将socket放入数组)
//4 把socket数组交给select管理
//5 检查s是否set集合的一名成员,如果是返回TRUE(检查当前socket是否在数组内,如果在就recvfrom,如果不在,传入下一个socket)
优点:可以同时处理多个套接字(select管理),正常情况下一次一个socket
缺点:a 依然阻塞:查看socket过程
将数据从内核拷贝到进程
b 占用CPU时间(不停的轮询)
c 管理的socket由数量限制(window下64个)
⑵ 异步选择(基于消息)注册消息
异步选择/异步事件
把请求注册给windows。
需要向windows提供的信息:socket,网络事件和消息。
a 在UDPNet.cpp里创建一个窗口用于接收消息,创建函数需要用窗口对象去调 用,而窗口的构造函数是受保护的不能在外部定义对象,因此采用单例模式创建接口,直接调用接口函数(获得窗口对象)
b 在UDPNet.cpp,创建之后注册消息(网络事件)
c 注册完成(定义一个RecvMsg消息,然后消息处理),通过异步来保证窗口可以干其他事。
//4 绑定成功
//1 创建窗口(消息的处理窗口)
//if(!CMyWnd::CreateMyWnd()->Create(NULL,"MyWnd"))
//{
// UnInitNetWork();
// returnfalse;
//}
//2 向windows注册消息 异步来保证窗口可以干其他事
//((CMyWnd*)CMyWnd::CreateMyWnd())->SetNet(this);
//WSAAsyncSelect(m_Socket,CMyWnd::CreateMyWnd()->m_hWnd,UM_RECVMSG,FD_READ);
优点:异步,注册完之后可以干其他事情了,
缺点:仍然是异步阻塞,从内核往进程拷贝数据时仍然阻塞。
移植性不好,必须基于某个窗口,不能跨平台;
速度慢,消息处理过程比较漫长,先经过系统的消息队列,再进入线程 消息队列,最后经过一系列过程到达处理函数。
⑶ 异步事件(异步阻塞)注册事件
向windows注册时提供的信息是:socket,网络事件和事件。某个socket发生网络事件时,会将事件置成有信号,从接收缓冲区拷贝数据即可。
实现步骤:①首先定义事件,
②然后等事件,正常情况下只能等一个事件(waitfor),在这里有可能发生多个网络事件,因此用WSA的等事件函数,可以协商等待几个事件,当事件有信号时线程处理,但是针对多个socket,则需要区分哪个socket对应着哪个事件,因此需要定义两个数组,通过索引的方式把socket和事件对应。
///异步事件(事件,线程)///
//4 绑定成功
//1向windows注册事件,注册成功后,把发生网络事件的句柄和socket放入数组,并统计个数++。
WSAEVENT hEventObject = WSACreateEvent(); //返回值就是发生网络事件的句柄
if(!WSAEventSelect(m_Socket,hEventObject,FD_READ))//返回值为0注册成功
{
m_aryEvent[m_EventNum] =hEventObject; //把发生网络事件的句柄放入数组
m_arySocket[m_EventNum] = m_Socket; //把发生网络事件的socket放入数组,并与句柄对应
m_EventNum++; //socket个数++
}
m_bFlag = TRUE;//循环结束条件
//2创建线程,等事件,处理发生网络事件的事件
//安全属性,栈的大小,函数地址,参数列表,执行标志,线程ID
m_hThreadRecv = (HANDLE)_beginthreadex(NULL, 0,&ThreadRecv,this,0,NULL);
nIndex=WSAWaitForMultipleEvents(pthis->m_EventNum,pthis->m_aryEvent,FALSE,WSA_INFINITE,TRUE);
缺点:有个数限制,API最多一次等64个事件,想一次等的更多,自己写函数。
⑷IO模型-------完成端口
原理:完成端口有一个socket队列,事先创建一个线程池,采用主动投递请求的方 式,当事件有信号时,就给他一个线程去执行任务(可以同时处理多个请求),另外,采用主动投递请求的方式,投递出去的是一个结构体,里面有(socket,网络事件,缓冲区)从而也解决了阻塞的问题(有了自己的内核缓冲区)。
优势: select,异步选择,异步事件,在从内核缓冲区向用户区读取数据时仍然是阻塞的,他们都不能同时处理多个socket请求。因此引入完成端口:
①采用线程池同时处理多个请求,
②采用主动投递请求解决阻塞的问题。
实现步骤:
①首先加载库,
②创建socket,bind,listen,
③在accept之前,先注册一个完成端口,
④然后把监听到的socket交给完成端口管理,
⑤然后采用主动投递请求的形式把结构体和socket投递出去;
⑥然后创建一个线程池来处理投递出去的请求。
每post一次就要new出一个结构体,如果不对结构体进行管理,就会内存泄漏。
18 文件映射
用途:主要用于读写大文件(好处:不用执行文件IO操作,也不需要文件内容做缓存操作,就好像被映射的文件都加载在内存中一样,非常适合管理大文件。)
原理:将磁盘文件的数据直接映射到进程地址空间中,并没有进行数据拷贝。从而实现进程和内核之间直接通信。
使用方法:①创建一个文件内核对象,指向磁盘上要作为内存映射的文件;
②创建一个文件映射内核对象,并告诉系统,文件的大小及访问该文件的方式(读/写);
③告诉系统,把文件映射内核对象的部分或者全部,映射到进程地址空间中。
④使用后:撤销文件映射内核对象的映像,关闭文件映射内核对象,关闭文件内核对象。
进程间通信:由一个进程创建,多个进程都可以访问,从而实现进程间的通信。
19 多线程编程下,强制杀死线程,会发生什么情况,请列举。怎么解决?
⑴死锁
①一般锁死锁:当多线程时,一个线程获取了一个锁,正在访问共享方法的时候(比如调用一个API时),还没来得及解锁,该线程就被强杀了,那这个锁永远不会被解开了,于是所有依赖这个锁的 其他线程都死锁了。
②库函数调用死锁:例如线程函数for循环,让程序显示出线程正在printf,主函数中创建线程的下一句是强杀线程,再下一句是printf,则第二个printf无法打印,并且printf死锁了,因为printf语句内部会在输出的时候加锁,而强杀没有给他解锁的机会。
⑵资源回收
当强杀一个线程时,它使用的所有资源也被回收,而有些可以复用的资源也可能被回收了。
⑶资源不能被释放
①线程的初始栈不能释放,认为线程没结束;
②依附于该线程的库也收不到通知,不能释放。
⑷解决办法
要考虑主动释放资源,调用强杀之前,先避开一些内存泄漏和死锁的问题,如果处于阻塞状态要先中断。
20程序执行时分为哪几个过程?
预处理①解析条件预处理命令 #ifdef #ifndef #define #endif
②#include将导入文件放到当前位置
③删除注释// /* */
④将文件序号标识
⑤#progma保留处理
(.c - .i)
编译:将预处理后的文件进行词法解析,语法解析,语义分析,编译优化,代码生成为汇编;(.i - .s)
汇编:将汇编代码转换为机器码语言(.s - .o/.obj(Linux))
链接:将机器码语言文件合成.exe(.o .lib/.a(Linux) - .exe)
执行:./执行
21 动/静态库
库是写好的现有的,成熟的,可以复用的代码。本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。
库有两种:静态库(.a、.lib)和动态库(.so、.dll)。
静态、动态是指链接。
大一点的项目会编写makefile文件(CMake等等工程管理工具)来生成静态库,输入多个命令太麻烦了。
静态库的特点:a静态库对函数库的链接是放在汇编时期完成的;
b程序在运行时与函数库不再接触,移植方便;
c浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件,执行效率高;
d静态库升级比较麻烦,如果某个静态库更新了,所有使用它的应用程序都需要重新编译、发布给用户,(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)。
动态库的特点:a动态库在程序运行时才被载入到目标代码中;
b动态库升级比较简单,动态库在程序运行是才被载入,用户只需要更新动态库即可,增量更新;
c可以实现进程之间的资源共享。
不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。
㈠静态库的生成:将写好的.cpp和.h等文件经过预编译,编译,汇编后生成的.o文件的集合,通过编号、索引、压缩打包后形成的一个(libxxx.a\libxxx.lib)文件。如下图所示。
静态链接:就是将生成的.o文件和引用到的库经过链接,一起打包到可执行文件中,对应的链接方式是静态链接,
⑴Linux下
①静态库生成过程:
#gcc –c 11.c
#gcc –c 12.c
#ar r libxxx.a 11.o 12.0
就生成了静态库 libxxx.a。大一点的项目会编写makefile文件(CMake等等工程管理工具)来生成静态库,输入多个命令太麻烦了。
Linux静态库命名规范:必须是"lib[your_library_name].a":lib为前缀,中间是静态库名,扩展名为.a
②静态库使用过程:
#gcc –c main.c –omain.o //把mian.c编译成目标文件
#gccmain.o –o qqq –L –lwxw //把main.o和libwxw.a库文件一起链接到执行程序中
-L:表示要连接的库所在目录,
qqq:最终执行文件的名字
-l:指定链接时需要的动态库,编译器查找动态连接库时有隐含的命名规则,即在给出的名字前面加上lib,后面加上.a或.so来确定库的名称。
⑵windows下
①静态库生成过程:
使用VS工程设置就很方便。
创建win32控制台程序时,勾选静态库类型;
打开工程“属性面板”—”配置属性”—”常规”,配置类型选择静态库。Build项目即可生成静态库。
②静态库使用过程:
打开工程“属性面板”—”配置属性”—“链接器”—”命令行”,输入静态库的完整路径即可。
㈡动态库的生成
⑴Linux下
①动态库生成过程:
#gcc –c 11.c
#gcc –c 12.c
#gcc –shared –o libxxx.so 11.o 12.0
就生成了动态库 libxxx.so。
-shared指定生成动态链接库。
Linux动态库命名规范: lib为前缀,中间是静态库名,扩展名为.so
②动态库使用过程:
⒈如果安装在/lib或者/usr/lib下,那么ld默认能够找到,无需其他操作。
⒉如果安装在其他目录,需要将其添加到/etc/ld.so.cache文件中,步骤如下: a编辑/etc/ld.so.conf文件,加入库文件所在目录的路径;
b运行ldconfig ,该命令会重建/etc/ld.so.cache文件。
⒊根据情况进行以上操作之后:
#gcc main.o –o qqq–L –lwxw
./执行即可。
⑵windows下
①动态库生成过程:
通常在导出函数的声明时需要有_declspec(dllexport)关键字:
生成动态库需要设置工程属性,打开工程“属性面板”—”配置属性”—”常规”,配置类型选择动态库。Build项目即可生成动态库。
②动态库使用过程:
工程“属性面板”—“通用属性”—“框架和引用”—”添加引用”,将显示“添加引用”对话框。“项目”选项卡列出了当前解决方案中的各个项目以及可以引用的所有库。 在“项目”选项卡中,选择 DynamicLibrary。 单击“确定”。
添加DynamicMath.h 头文件目录,必须修改包含目录路径。打开工程 “属性面板”—”配置属性”—“C/C++”—”常规”,在“附加包含目录”属性值中,键入DynamicMath.h 头文件所在目录的路径或浏览至该目录。编译运行即可OK。
22 消息机制
⑴消息分类:
1首先定义一个消息#define
2在主窗口中建立映射(将函数和消息建立映射)ON_MESSAGE
3 函数实现(通过ON_MESSAGE找到函数)
⑵消息的产生;地址:
从0x0000到 0x03FF,为系统定义的消息,常见的 WM_PAINT、WM_CREATE 等均在 其中;
从0x0400 到 0x7FFF,专用于用户自定义的消息,可以使用 WM_USER + x 的形式自行定义,其中WM_USER 的值就是 0x0400,x 取一个整数;
从0x8000 到 0xBFFF,也用作用户自定义的消息范围,可以使用 WM_APP + x 的形式自行定义。
⑶消息处理API
⑴消息处理要经历几个过程:消息发送之后首先经过系统的消息队列,再进入线程的消息队列,最后经过一系列的处理过程到达处理函数。
⑵消息发送:
发送消息可以附加一些信息在WPARAM LPARAM中附带过去
::SendMessage(GetParent()->m_hWnd,UM_DESTROYMSG,(WPARAM)&strIp,0);
向FeiQDlg窗口发送一个销毁窗口的消息,并把想要删除的IP写在附加消息中带过去;
SendMessage():SendMessage函数将指定的消息发到窗口。它调用特定窗口的窗口处理函数(即WndProc函数),并且不会立即返回,直到窗口处理函数处理了这个消息。
PostMessage():PostMessage函数将一个消息放入与创建这个窗口的消息队列相关的线程中,并立刻返回不等待线程处理消息,至于消息何时被处理,PostMessage完全不知道。
⑶消息接收
消息循环:
Ln000:while (GetMessage(&msg, NULL, 0, 0))
Ln001:{
Ln002: TranslateMessage(&msg);
Ln003: DispatchMessage(&msg);
Ln004:}
消息处理函数:
Ln100:LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAMwParam, LPARAM lParam)
wParam的前4个字节就是消息类型,然后做出对用的处理
Ln101:{
Ln102: int wmId, wmEvent;
Ln103: switch (message)
Ln104: {
Ln105: case WM_COMMAND:
23.C++是不是类型安全的?
答案:不是。两个不同类型的指针之间可以强制转换(用reinterpret cast)。C#是类型安全的。
24. 基类的析构函数不是虚函数,会带来什么问题?
【参考答案】派生类的析构函数用不上,会造成资源的泄漏。
25. “引用”与多态的关系?
引用是除指针外另一个可以产生多态效果的手段。这意味着,一个基类的引用可以指向它的派生类实例。
例4
Class A; Class B : Class A{...}; B b;A& ref = b;
26.内存泄露及解决办法
⑴什么是内存泄漏(memoryleak)?
指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
⑵对于C和C++这种没有Garbage Collection 的语言来讲,我们主要关注两种类型的内存泄漏:
①堆内存泄漏(Heap leak)。对内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.
②系统资源泄露(Resource Leak)。主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。
⑶如何解决内存泄露?
内存泄露的问题其困难在于1.编译器不能发现这些问题。2.运行时才能捕获到这些错误,这些错误没有明显的症状,时隐时现。3.对于手机等终端开发用户来说,尤为困难。
下面从三个方面来解决内存泄露:
第一:良好的编码习惯,尽量在涉及内存的程序段,检测出内存泄露。当程式稳定之后,在来检测内存泄露时,无疑增加了排除的困难和复杂度。
第二:重载new 和 delete。这也是大家编码过程中常常使用的方法。
⑷由内存泄露引出内存溢出话题:
所谓内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是会产生内存溢出的问题。
常见的溢出主要有:
①内存分配未成功,却使用了它。
常用解决办法是,在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查。如果是用malloc 或new 来申请内存,应该用if(p==NULL)或if(p!=NULL)进行防错处理。
②内存分配虽然成功,但是尚未初始化就引用它。
③内存分配成功并且已经初始化,但操作越过了内存的边界。
例如在使用数组时经常发生下标“多1”或者“少1”的操作。特别是在for 循环语句中,循环次数很容易搞错,导致数组操作越界。
④使用free 或delete 释放了内存后,没有将指针设置为NULL。导致产生“野指针”。
程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。