v 题型:
数据结构(链表,数,图)、C++编程基础、数组、递归、矩阵、内存管理、时间复杂度、TCP/IP、操作系统、计算机网络、UML、OOA&OOP、自己的项目
v swap的几种写法:
void swap(int* a, int* b) |
||
{ int temp; temp = *a; *a = *b; *b = temp; } |
{ *a = *a + *b; *b = *a - *b; *a = *a - *b; } |
{ *a = *a ^ *b; *b = *b ^ *a; *a = *a ^ *b; } |
v 如何在C++中调用C程序:
C++和C是两种完全不同的编译链接处理方式,如果直接在C++里面调用C函数,会找不到函数体,报链接错误。要解决这个问题,就要在 C++文件里面显示声明一下哪些函数是C写的,要用C的方式来处理。
1.引用头文件前需要加上 extern “C”,如果引用多个,那么就如下所示
extern “C”
{
#include “ s.h”
#include “t.h”
#include “g.h”
#include “j.h”
};
然后在调用这些函数之前,需要将函数也全部声明一遍。
2.C++调用C函数的方法,将用到的函数全部重新声明一遍
extern “C”
{
extern void A_app(int);
extern void B_app(int);
extern void C_app(int);
extern void D_app(int);
}
C++程序中调用被c编译器编译后的函数,为什么要加extern "C"?
C++语言支持函数重载,C语言不支持函数重载。函数被C++编译后在库中的名字与C语言的不同。假设某个C 函数的声明如下:
void foo(int x, int y);
该函数被C 编译器编译后在库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字用来支持函数重载和类型安全连接。由于编译后的名字不同,C++程序不能直接调用C 函数。C++提供了一个C 连接交换指定符号extern“C”来解决这个问题。例如:
extern “C”
{
void foo(int x, int y);
// 其它函数
}
或者写成
extern “C”
{
#include “myheader.h”
// 其它C 头文件
}
这就告诉C++编译译器,函数foo 是个C 连接,应该到库中找名字_foo而不是找_foo_int_int。C++编译器开发商已经对C 标准库的头文件作了extern“C”处理,所以我们可以用#include直接引用这些头文件。
v 操作符优先级
优先级 |
运算符 |
名称或含义 |
使用形式 |
结合方向 |
说明 |
1 |
[] |
数组下标 |
数组名[常量表达式] |
左到右 |
|
() |
圆括号 |
(表达式)/函数名(形参表) |
|||
. |
成员选择(对象) |
对象.成员名 |
|||
-> |
成员选择(指针) |
对象指针->成员名 |
|||
2 |
- |
负号运算符 |
-表达式 |
右到左 |
单目 |
(类型) |
强制类型转换 |
(数据类型)表达式 |
|||
++ |
自增运算符 |
++变量名/变量名++ |
单目 |
||
-- |
自减运算符 |
--变量名/变量名-- |
单目 |
||
* |
取值运算符 |
*指针变量 |
单目 |
||
& |
取地址运算符 |
&变量名 |
单目 |
||
! |
逻辑非运算符 |
!表达式 |
单目 |
||
~ |
按位取反运算符 |
~表达式 |
单目 |
||
sizeof |
长度运算符 |
sizeof(表达式) |
|||
3 |
/ |
除 |
表达式/表达式 |
左到右 |
双目 |
* |
乘 |
表达式*表达式 |
双目 |
||
% |
余数(取模) |
整型表达式/整型表达式 |
双目 |
||
4 |
+ |
加 |
表达式+表达式 |
左到右 |
双目 |
- |
减 |
表达式-表达式 |
双目 |
||
5 |
<< |
左移 |
变量<<表达式 |
左到右 |
双目 |
>> |
右移 |
变量>>表达式 |
双目 |
||
6 |
> |
大于 |
表达式>表达式 |
左到右 |
双目 |
>= |
大于等于 |
表达式>=表达式 |
双目 |
||
< |
小于 |
表达式<表达式 |
双目 |
||
<= |
小于等于 |
表达式<=表达式 |
双目 |
||
7 |
== |
等于 |
表达式==表达式 |
左到右 |
双目 |
!= |
不等于 |
表达式!= 表达式 |
双目 |
||
8 |
& |
按位与 |
表达式&表达式 |
左到右 |
双目 |
9 |
^ |
按位异或 |
表达式^表达式 |
左到右 |
双目 |
10 |
| |
按位或 |
表达式|表达式 |
左到右 |
双目 |
11 |
&& |
逻辑与 |
表达式&&表达式 |
左到右 |
双目 |
12 |
|| |
逻辑或 |
表达式||表达式 |
左到右 |
双目 |
13 |
?: |
条件运算符 |
表达式1? 表达式2: 表达式3 |
右到左 |
三目 |
14 |
= |
赋值运算符 |
变量=表达式 |
右到左 |
|
/= |
除后赋值 |
变量/=表达式 |
|||
*= |
乘后赋值 |
变量*=表达式 |
|||
%= |
取模后赋值 |
变量%=表达式 |
|||
+= |
加后赋值 |
变量+=表达式 |
|||
-= |
减后赋值 |
变量-=表达式 |
|||
<<= |
左移后赋值 |
变量<<=表达式 |
|||
>>= |
右移后赋值 |
变量>>=表达式 |
|||
&= |
按位与后赋值 |
变量&=表达式 |
|||
^= |
按位异或后赋值 |
变量^=表达式 |
|||
|= |
按位或后赋值 |
变量|=表达式 |
|||
15 |
, |
逗号运算符 |
表达式,表达式,… |
左到右 |
从左向右顺序运算 |
v ::在C++中是什么意思
::是运算符中等级最高的,它分为三种:
1)global scope(全局作用域符),用法(::name)
2)class scope(类作用域符),用法(class::name)
3)namespace scope(命名空间作用域符),用法(namespace::name)
他们都是左关联(left-associativity)
他们的作用都是为了更明确的调用你想要的变量,如在程序中的某一处你想调用全局变量a,那么就写成::a,如果想调用class A中的成员变量a,那么就写成A::a,另外一个如果想调用namespace std中的cout成员,你就写成std::cout(相当于using namespace std;cout)意思是在这里我想用cout对象是命名空间std中的cout(即就是标准库里边的cout)
v i++
!x++; !与++优先级相同,先计算!x;然后计算x++; (原题:P35)
v 指针+1的问题
指针变量加1,即向后移动1 个位置表示指针变量指向下一个数据元素的首地址。而不是在原地址基础上加1。至于真实的地址加了多少,要看原来指针指向的数据类型是什么。
p指向的是一个字符,p+1就是移动一个字符大小,一个字符就是一个字节,所以p +1 代表的地址就比 p 代表的地址大1。
p指向的是一个整型,p+1就是移动一个整型大小,即移动4个字节,所以p+1代表的地址比p代表的地址大4.
v x++与++x
#include <stdio.h> void main() { int arr[] = { 6, 7, 8, 9, 10 }; int *ptr = arr; *(ptr++) += 123;//equal:*ptr=*ptr+123;ptr++; printf("%d,%d\n",*ptr,*(++ptr));//prinft计算参数时从右向左入栈,先执行++ptr; } RESULT:8,8 X++先执行运算最后++;++X则先++然后执行其他运算; |
v 隐式类型转换发生在下列这些典型的情况下:
- 在混合类型的算术表达式中(最宽的数据类型成为目标类型)。
- 用一种类型的表达式赋值给另一种类型的对象。
- 把一个表达式传递给一个函数,调用表达式的类型与形式参数的类型不同。
- 从一个函数返回一个表达式的类型与返回类型不相同。
v 判断一个数X是否是2N次方,不可用循环语句:
!(X&(X-1))==0
v Const
在C程序中,const的用法主要是定义常量、修饰函数参数、修饰函数返回值,在C++中,它还可以修饰函数的定义体,定义类中某个成员函数为恒态函数,即不改变类中的数据成员
被Const修饰的东西可以受到强制保护,预防意外的变动,能提高程序的健壮性
Const与#define相比有什么不同?
Const常量有数据类型,而宏常量没有数据类型,编译器可以对前者进行类型安全检查,而对后者只能进行字符替换,没有类型安全检查,并且在字符替换中可能产生意料不到的错误
调试工具能对const常量进行调试,但是无法对define常量调试
No. |
作用 |
说明 |
参考代码 |
1 |
可以定义const常量 |
|
const int Max = 100; |
2 |
便于进行类型检查 |
const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换时可能会产生意料不到的错误 |
void f(const int i) { .........} |
3 |
可以保护被修饰的东西 |
防止意外的修改,增强程序的健壮性。 |
void f(const int i) { i=10;//error! } |
4 |
可以很方便地进行参数的调整和修改 |
同宏定义一样,可以做到不变则已,一变都变 |
|
5 |
为函数重载提供了一个参考 |
|
class A |
6 |
可以节省空间,避免不必要的内存分配 |
const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝 |
#define PI 3.14159 //常量宏 |
7 |
提高了效率 |
编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高 |
|
const int *A; 或 int const *A; //const修饰指向的对象,A可变,A指向的对象不可变
int *const A; //const修饰指针A, A不可变,A指向的对象可变
const int *const A; //指针A和A指向的对象都不可变
v sizeof与Strlen()
sizeof 单位:字节
sizeof(...)是运算符,而不是一个函数。一个简单的例子:
int a;
cout<<sizeof a<<endl;
在头文件中typedef为unsigned int,其值在编译时即计算好了,参数可以是数组、指针、类型、对象、函数等。
它的功能是:获得保证能容纳实现所建立的最大对象的字节大小。
由于在编译时计算,因此sizeof不能用来返回动态分配的内存空间的大小。
实际上,用sizeof来返回类型以及静态分配的对象、结构或数组所占的空间,返回值跟对象、结构、数组所存储的内容没有关系。
具体而言,当参数分别如下时,sizeof返回的值表示的含义如下:数组——编译时分配的数组空间大小;
指针——存储该指针所用的空间大小(存储该指针的地址的长度,是长整型,应该为4);类型——该类型所占的空间大小;
对象——对象的实际占用空间大小;
函数——函数的返回类型所占的空间大小。函数的返回类型不能是void。
********************************************************************
strlen
strlen(...)是函数,要在运行时才能计算。
参数必须是字符型指针(char*), 且必须是以'\0'结尾的。当数组名作为参数传入时,实际上数组就退化成指针了。
int ac[10];
cout<<sizeof(ac)<<endl;
cout<<strlen(ac)<<endl; (ac相当于一个指针,但是strlen只能接受char*类型,所以编译时出错)
它的功能是:返回字符串的长度。该字符串可能是自己定义的,也可能是内存中随机的,该函数实际完成的功能是从代表该字符串的第一个地址开始遍历,直到遇到结束符'\0'。返回的长度大小不包括'\0'。
v 结构体长度对齐
在默认情况下,为了方便对结构体内元素的访问和管理,当结构体内的元素长度都小于处理器的位数的时候,便以结构体里面最长的数据元素为对其单位,也就是说,结构体的长度一定是最长的数据元素的整数倍;如果结构体内存在长度大于处理器位数的元素,那么就以处理器的位数为对齐单位。但是结构体内类型相同的连续元素将在连续的空间内,和数组一样。
CPU的优化规则大致是这样:对于n字节的元素(n=2,4,8,…),他的首地址能被n整除,才能获得最好的性能。(个人感觉是如果不是这样,那么一个元素很有可能会跨页存储,存在两个不同的页面上,导致访问速度慢)
v 对其是一种以空间换时间的方法,在访问内存时,如果地址按4字节对齐,则访问效率会高很多,这种现象的原因在于访问内存的硬件电路。一般情况下,地址总线总是按照对齐后的地址来访问的。例如你想得到0x00000001开始的4字节内容,系统需先以0x00000000读4个字节,从中取3字节,然后再用0X00000004最为开始地址,获得下一个四字节,再从中得到第一个字节,两次组合出你想要的内容。但是如果地址一开始就是对齐到0x00000000,则系统只需一次内存读写即可。
v 对其的自定义设置
#pragma pack(1)
struct aStruct…{…};
#pragma pack()
v 字节对齐是在编译时决定的,不会再发生变化,在运行时不会再发生变化
v 静态变量存放在全局数据区,而sizeof计算栈中分配的大小,是不会计算static变量的。
v 数据类型的长度:
short -------------- 2
int,long,float,指针---4
double -------------- 8
v sizeof vs strlen
sizeof是算符,strlen是函数
sizeof可以用类型做参数,strlen只能用char*做参数,且必须是以“\0”结尾的。
数组做sizeof的参数不退化,传递给strlen就退化为了指针
sizeof在编译时计算,strlen在运算时计算
对函数使用sizeof,在编译阶段会被函数返回值类型取代。
v sizeof不是函数,也不是一元运算符,他是个类似宏定义的特殊关键字,sizeof()括号内的内容是不被编译的,只是替换,所以a=8;sizeof(a=6);之后a的值仍然为8。
v unsigned影响的仅仅是最高位bit的意义(正/负),而不会影响数据长度。
v 空类所占空间为1,单一继承的空类空间也为1,多重继承的空类空间还是1,但是虚继承涉及到虚表(虚指针),所以空间大小为4
v 内联函数和宏定义
内联函数和普通函数相比可以加快程序运行的速度,因为不需要中断调用,在编译的时候内联函数可以直接被镶嵌到目标代码中,而宏只是一个简单的替换;内联函数要做参数类型检查,这是inline和宏相比的优势;
inline一般只用于如下情况:
- 一个函数不断被重复调用;
- 函数只有简单的几行,且函数内不包括for/while/switch语句;
—————————————————————————————————————————————————————————————————————————————————————————
v 指针问题:包括常量指针、数组指针、函数指针、this指针、指针传值、指向指针的指针等问题
v 指针和引用的区别
非空区别:在任何情况下都不能使用指向空值的引用;
合法性区别:在使用引用之前不需要测试它的合法性;
可修改区别:指针可以被重新赋值以指向另一个不同的对象,但是引用则总是指向初始化时被指定的对象,以后不能改变,但是指定的内容可以改变;
应用区别:使用指针:存在不指向任何对象的可能;需要 在不同的时刻指向不同的对象;使用引用:总是指向一个对象并且一旦指向一个对象后就不会改变指向。
v 引用的初始化:
声明一个引用时,必须同时初始化,如同const常量必须声明的同时初始化。
v char c[] vs char *c
char c[]分配的是局部数组,而char *c分配的是全局数组
v 函数指针
函数指针 |
void (*f)() |
函数返回指针 |
void* f() |
const指针 |
cons int * |
指向const的指针 |
int * const |
指向const的const指针 |
cons int* const |
v malloc和free是C/C++的标准库函数,new/delete是C/C++的运算符。他们都可以用于动态申请内存和释放内存。对于非内部数据类型的对象而言,光用malloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限范围内,不能把执行构造函数和析构函数的任务强加于malloc/free
v 数组指针和指针数组:
数组指针 |
指针数组 |
int (*a)[] |
int *a[] |
—————————————————————————————————————————————————————————————————————————————————————————
v STL模版与容器
_________________
v STL的优点:
- 方便容易的实现搜索数据或对数据排序等一系列的算法
- 调试程序是更安全和方便
- 即使是人们用STL在UNIX平台下写的代码,你也可以很容易理解
v STL中的一些基本概念
- 模版(Template)类的宏(macro),正规名:泛型,类的模版-泛型类,函数模版-泛型函数
- STL标准模版库,一些聪明人自己写的一些模版
- 容器(Container):可容纳一些数据的模版类,STL中有vector, set, map, multimap 和 deque等容器
- 向量(Vector):基本数组模版,是一个容器
- 游标(Iterator):它是一个指针,用来指向STL容器中的元素,也可以指向其他元素
v 容器向量:标准模板库是一个基于模版的容器类库,包括链表、列表、队列和堆栈;标准模板库还包括许多常用的算法,包括排序和查找;可重用;容器是包括其他对象的对象;标准模版库类有两种类型:顺序和关联;不同OS间可移植;
v vector的操作:
标准库vector类型使用需要的头文件:#include <vector>。vector 是一个类模板。不是一种数据类型,vector<int>是一种数据类型。Vector的存储空间是连续的,list不是连续存储的。
- 定义和初始化
vector< typeName > v1; //默认v1为空,故下面的赋值是错误的v1[0]=5;
vector<typeName>v2(v1); 或v2=v1 ; 或vector<typeName>v2(v1.begin(), v1.end());//v2是v1的一个副本,若v1.size()>v2.size()则赋值后v2.size()被扩充为 v1.size()。
vector< typeName > v3(n,i);//v3包含n个值为i的typeName类型元素
vector< typeName > v4(n); //v4含有n个值为0的元素
int a[4]={0,1,2,3,3}; vector<int> v5(a,a+5);//v5的size为5,v5被初始化为a的5个值。后一个指针要指向将被拷贝的末元素的下一位置。
vector<int> v6(v5);//v6是v5的拷贝。
vector< 类型 > 标识符(最大容量,初始所有值)。
- 值初始化
1> 如果没有指定元素初始化式,标准库自行提供一个初始化值进行值初始化。
2> 如果保存的是含有构造函数的类类型的元素,标准库使用该类型的构造函数初始化。
3> 如果保存的是没有构造函数的类类型的元素,标准库产生一个带初始值的对象,使用这个对象进行值初始化。 - vector对象最重要的几种操作
1. v.push_back(t) 在容器的最后添加一个值为t的数据,容器的size变大。另外list有push_front()函数,在前端插入,后面的元素下标依次增大。
2. v.size() 返回容器中数据的个数,size返回相应vector类定义的size_type的值。v.resize(2*v.size)或v.resize(2*v.size, 99) 将v的容量翻倍(并把新元素的值初始化为99)
3. v.empty() 判断vector是否为空
4. v[n] 返回v中位置为n的元素
5. v.insert(pointer,number, content) 向v中pointer指向的位置插入number个content的内容。
还有v. insert(pointer, content),v.insert(pointer,a[2],a[4])将a[2]到a[4]三个元素插入。
6. v.pop_back() 删除容器的末元素,并不返回该元素。
7.v.erase(pointer1,pointer2) 删除pointer1到pointer2中间(包括pointer1所指)的元素。vector中删除一个元素后,此位置以后的元素都需要往前移动一个位置,虽然当前迭代器位置没有自动加1,但是由于后续元素的顺次前移,也就相当于迭代器的自动指向下一个位置一样。
8. v1==v2 判断v1与v2是否相等。
9. !=、<、<=、>、>= 保持这些操作符惯有含义。
10. vector<typeName>::iterator p=v1.begin( ); p初始值指向v1的第一个元素。*p取所指向元素的值。对于const vector<typeName>只能用vector<typeName>::const_iterator类型的指针访问。
11. p=v1.end( ); p指向v1的最后一个元素的下一位置。
12.v.clear() 删除容器中的所有元素。
- #include<algorithm>中的泛函算法
搜索算法:find() 、search() 、count() 、find_if() 、search_if()、count_if()
分类排序:sort() 、merge()
删除算法:unique() 、remove()
生成和变异:generate() 、fill() 、transformation() 、copy()
关系算法:equal() 、min() 、max()
sort(v1.begin(),vi.begin()+v1.size/2); 对v1的前半段元素排序
list<char>::iterator pMiddle =find(cList.begin(),cList.end(),'A');找到则返回被查内容第一次出现处指针,否则返回end()。
vector< typeName >::size_type x ; vector< typeName >类型的计数,可用于循环如同for(int i)
初学C++的程序员可能会认为vector的下标操作可以添加元素,其实不然:
vector<int> ivec; // empty vector
for (vector<int>::size_typeix = 0; ix != 10; ++ix)
ivec[ix] = ix; // disaster: ivec has no elements
上述程序试图在ivec中插入10个新元素,元素值依次为0到9的整数。但是,这里ivec是空的vector对象,而且下标只能用于获取已存在的元素。
这个循环的正确写法应该是:
for (vector<int>::size_typeix = 0; ix != 10; ++ix)
ivec.push_back(ix); // ok: adds new element with value ix
警告:必须是已存在的元素才能用下标操作符进行索引。通过下标操作进行赋值时,不会添加任何元素,仅能对确知已存在的元素进行下标操作 。
v 泛型编程
何谓泛型编程:STL代表用一致的方式编程是可能的;泛型编程是一种基于发现高效算法的最抽象表示的编程方法;STL是一个泛型编程的例子,C++是我们可以实现令人信服的例子的语言。
v 模版
一个函数在编译时被分配给一个入口地址,这个入口地址就称为函数的指针,正如指针是一个变量的地址一样。(经测试:函数名和对函数名取地址得到的是同一个值,都可以作为该函数的指针)。有些地方必须使用函数指针才能完成给定的任务,特别是异步操作的回调和其他需要匿名回调的结构。另外,想线程的执行和时间的处理,如果缺少了函数的支持也是很难完成的。
—————————————————————————————————————————————————————————————————————————————————————————————
v 面向对象(Object-Oriented)
编程是在计算机中反应世界
面向对象的优点:良好的可复用性、易维护、良好的可扩充性
面向对象技术的基本概念是:对象、类和继承
C++中的空类默认产生的成员函数:
默认构造函数、析构函数、拷贝构造函数、赋值函数
C++中struct也可以有constructor/destructor及成员函数,它和class的区别是:class的默认访问控制是private,struct中默认访问控制是public。
在一个类中,初始化列表的初始化变量顺序是根据成员变量的声明顺序来执行的。
MFC库中,为什么CObject的析构函数是虚函数?
保证在任何情况下,不会出现由于析构函数未被调用而导致内存泄漏
析构函数可以是内联函数,但内联函数不能为virtual
多态:允许将子类类型的指针赋值给父类类型的指针,多态在ObjectPascal和C++中都是通过虚函数(VirtualFunction)实现的。
- overload vs override:
- overload: 范围相同,参数不同,静态,函数调用在编译期间确定,早绑定,与面向对象无关,与多态无关。
- override:范围不同,参数相同,子类重新定义父类的虚函数,函数动态调用、运行期绑定。
实现代码重用 |
封装:隐藏实现细节,使得代码模块化
继承:扩展已存在的代码模块(类)
多态:同一操作作用于不同对象上,产生不同的解释和执行结果—实现接口重用
友元函数:定义在类外部的普通函数,但他需要在类体内说明,为了与该类的成员函数区别,说明时需要在前面加上friend来区别;他虽然不是成员函数,但却可以访问类的私有变量。
友元还可以是类,称为友元类
继承:可以使一个新类获得其父类的操作和数据结构,程序员只需在新类中增加原有类中没有的成分。
为了不破坏类的封装性,所有操作应该通过接口进行沟通。
类中的static变量存在数据段,不在类的实例空间中。