语言总结—C/C++
参考《程序员面试宝典》
1. 基本概念
1.1 赋值语句
例1. 按位与操作,例如:a=3,b=3,a&b值等于 0011 & 0011 结果还是0011,那么值还是3; a=4,b=3,a|b:按位或操作, 0100 | 0011 结果是0111,输出的值为7;a||b, a和b进行或运算,只要两者有一个为真则结果即为1.
例2. while(x) { count++; x=x&(x-1);} 此循环用来求输入值x转化为二进制后1的个数。eg:9(1001)&8(1000)=8(1000)=>8(1000)&7(0111)=>0 循环两次,有2个1.
1.2 i++
例1. for(a=0,x=0;a<=1 && !x++; a++){a++;} 和 for(a=0,x=0;a<=1 && !x++; ){a++;} 前式进入循环体前a和x均自增为1,再a++后a就为2,循环判别条件a<=1不符合即不进行右边的操作,结果为a=2,x=1;而后式结果a=1,x=2.
例2.
main(){ int arr[]={6,7,8,9,10}; int *ptr=arr; *(ptr++)+=123; printf("%d,%d\n",*ptr,*(++ptr)); }
C中printf计算参数时是从右往左压栈的。 第2句使得ptr指向第一个元素6;第3句等价于 *ptr=*ptr+123;ptr++, 第一个元素值应为129,而ptr指向第二个元素7;第4句从右往左运算,首先++ptr等价于ptr++,此时ptr指向第三个元素8,*ptr=8,所以全部输出为8.
1.3 类型转换
例1. float a=1.0f; count << (int)a <<endl; 结果输出为1065353216,不是1,因为浮点数在内存里和整数的存储方式不同,(int&)a相当于将该浮点数地址开始的sizeof(int)个字节当成int型的数据输出,因此这取决于float型数据在内存中的存储方式,而不是经过(int&)a显示转换的结果1.
unsigned int 变量赋值给unsigned char变量时会发生字节截断(3位和高于3位的将被程序自动丢弃)
隐式类型转换:混合算术表达(int类型和double类型数据相加为double类型)、用一种类型的表达式赋值给另一种类型的对象、表达式传值给目标函数但与形参类型不同(形参类型为目标转换类型)、函数返回表达式类型与函数类型不一致(转换为函数返回值类型)
准则:1)为防止精度丢失,类型总被提升为较宽类型 eg.char(向上提升一般转为ASCI码,如’a’为97)->int->float->double->long double
2)所有含有小于整型的有序类型的算术表达式在计算之前其类型都会被转换成整型
1.4 运算符问题
例1. char a=0xA5;char b=~a>>4+1; 优先级顺序 ~优于+优于>>,所以先执行取反操作 1010 0101 取反 0101 1010,再直接右移5位得0000 0010.
A - 65 - 0100 0001;a – 97 – 0110 0001; 可显示字符;0x是十六进制前缀,0开头是八进制。
例2. 利用位运算实现两个整数加法
int add(int a,int b){ if(b==0) return a; //没有进位的时候完成运算 int sum,carry; sum = a ^ b;//完成第一步没有进位的加法运算 carry=(a & b) << 1;//完成第二步进位并且左移运算 return add(sum,carry);//递归,相加 }
1.5 交换与比较
例1. 不用交换和判断语句找出两个数中间比较大的.
int max = ((a+b)+abs(a-b))/2
例2. 不用排序给出三个数的中间的那个数。
int max(int a,int b) {return a>=b?a:b;} int min(int a,int b) {return a<=b?a:b;} int medium(int a,int b,int c){ int t1=max(a,b); int t2=max(b,c); int t3=max(a,c); return min(t1,min(t2,t3)); }
例3. 不使用中间变量交换a,b的值。
//法一 a=a+b;//可能出界 b=a-b; a=a-b; //法二 异或 a=a^b; b=a^b; a=a^b;
1.6 C和C++的关系
例1. 在C++程序中调用被C编译器编译后的函数,为什么要加extern “C”?答:C++语言支持函数重载,C语言不支持,函数被C++编译后在库中的名字与C语言不同,假设某个函数的原型为void foo(int x,int y),该函数被C编译器编译后在库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字。C++提供了C连接交换指定符号extern “C”解决名字匹配问题。
例2. C和C++。C是一种结构化语言,重点在于算法和数据结构,考虑的是通过一个过程如何将输入处理得到输出,而C++考虑的首先是一个对象模型,让这个模型能够契合与之对应的问题域,这样可以通过获取对象的状态信息得到输出或实现过程控制。
2.预处理、const与sizeof
2.1 宏定义
例1. 使用预处理指令#define声明一个常数,用以表明1年中有多少秒(忽略闰年)
#define SECOND_PER_YEAR (60 * 60 * 24 * 365) UL
没有结束分号,UL是无符号长整型, 防止在16位机的整型溢出。
例2. 写一个标准的宏MIN,输出最小值
直到嵌入inline操作符变为标准C的一部分,宏都是方便产生嵌入代码的唯一办法。
#define MIN(A,B) ( (A) <= (B) ? (A) : (B))
2.2 const
const 修饰指针,一般分为4种:
int b = 500; const int* a = &b; //情况1 int const *a = &b; //情况2 int* const a = &b; //情况3 const int* const a= &b; //情况4
情况1,2,如果const位于星号的左侧,则const用来修饰指针所指向的变量,即指针指向为常量;如果const位于星号的右侧,const就是修饰指针本身,即指针本身是常量。情况1,2都是指针所指向内容为常量,此时改变*a的值的办法要么改变b的值要么指向别处,且情况1,2定义时都可以先不进行初始化,因为虽指针内容是常量但指针本身不是常量。
int b=500; const int* a=&b; *a=600; //错误 b=600; cout<< *a <<endl;//得到600 int c=700; a=&c; cout<< *a <<endl;//得到700
情况3,指针本身是常量,不能对指针更改,且定义时必须同时初始化。
int b=500,c=600; int* const a;//错误,没有初始化 int* const a = &b; //正确,必须初始化 *a = 600; //正确,允许改值 cout<< a++ << endl;//错误
情况4,指针本身和指向的值均为常量,都不可以改变。
const成员函数:不改变数据成员的函数加上const关键字进行标识,可提高程序的可读性和可靠性,一旦企图修改数据成员的值,则编译器按错误处理。如getter函数,例int GetY() const; 且必须以同样的方式重复出现在函数实现里,表示成员值不可变,否则编译器会看成不同的函数。如果把const放在函数声明前,意味着函数的返回值是常量。另,在const成员函数中,用mutable修饰成员变量名后,就可以修改类的成员变量了。
例1.const和#define有什么不同?
C++可以用用const定义常量,也可以用#define定义常量,前者比后者有更多优点:1)const常量有数据类型,而宏常量没有数据类型,编译器可以对前者进行类型安全检查,后者只进行字符替换,没有类型安全检查,并且在字符替换中可能会产生意料不到的错误。 2)有些集成化的调试工具可以对const进行调试,但是不能对宏常量进行调试,在C++程序中只使用const常量而不使用宏常量,即const常量完全取代宏常量。
2.3 sizeof
例1.
#include <iostream> #include <stdio.h> #include <string.h> using namespace std; struct{ short a1; short a2; short a3; }A; struct{ long a1; short a2 }B; int main(){ char* ss1 = "0123456789"; char ss2[] = "0123456789"; char ss3[100] = "0123456789"; int ss4[100]; char q1[]="abc"; char a2[]="a\n"; char* q3="a\n"; char *str1 = (char *) malloc(100); void *str2 = (void *) malloc(100); cout<< sizeof(ss1) <<endl; cout<< sizeof(ss2) <<endl; cout<< sizeof(ss3) <<endl; cout<< sizeof(ss4) <<endl; cout<< sizeof(q1) <<endl; cout<< sizeof(q2) <<endl; cout<< sizeof(q3) <<endl; cout<< sizeof(A) <<endl; cout<< sizeof(B) <<endl; cout<< sizeof(str1) <<endl; cout<< sizeof(str2) <<endl; return 0; }
ss1是一个字符指针,指针的大小是一个空值,4字节;ss2是一个字符数组,由具体填充值来定,填充值是“0123456789”,1个字符所占空间是1个字节,再加上隐含的“\0”,一共是11字节;ss3也是一个字符数组,预分配100,所以100字节;ss4整形数组,预分配100,每个整型变量所占空间是4,所以一共是400字节;q1与ss2类似,4字节;q2里面有一个“\n”,算作一位,所以空间大小是3字节;q3是一个字符指针,指针的大小是一个定值,4字节;str1,str2均为指针,都为4字节。
A和B是两个结构体,默认情况下,为了方便对结构体内元素的访问和管理,当结构体内的元素的长度都小于处理器的位数的时候,便以结构体里面最长的数据元素为对齐单位,即,结构体的长度一定是最长的数据元素的整数倍,如果结构体内存在长度大于处理器位数的元素,那么就以处理器的位数为对齐单位,但是结构体内类型相同的连续元素将在连续的空间内,和数组一样。结构体A中有3个short类型变量,各自以2字节对齐,结构体对齐参数按默认的8字节对齐,则a1,a2,a3都取2字节对齐,sizeof(A)为6,也是2的倍数;结构体B中a1为4字节,a2为2字节,默认对齐参数是8,a1取4字节对齐,a2取2字节对齐,结构体大小为6字节,6不为4的整数倍,补空字节增到8,符合,所以sizeof(B)为8.
CPU的优化性能是这样的:对于n字节的元素(n=2,4,8,…),它的首地址能被n整除才能获得最好的性能。
例2. sizeof和strlen之间的区别
char* ss="0123456789"; sizeof(ss); //4,ss是指向字符串常量的字符指针 strlen(ss); //1,*ss是第一个字符 char ss[]="0123456789"; sizeof(ss); //11,ss是数组,计算了“\0”位置 strlen(ss); //1,*ss是第一个字符 char ss[100]="0123456789"; sizeof(ss); //100,ss表示在内存中预分配的大小,100*1=100 strlen(ss); //10,内部实现使用一个循环计算字符串的长度,知道“\0”为止 int ss[100]="0123456789"; sizeof(ss); //400,ss表示在内存中的大小,100*4=400 strlen(ss); //错误,strlen参数只能是char*,且必须以“\0”结尾
区别:1)sizeof操作符的结果类型为size_t,它在头文件中的typeof为unsigned int类型,保证能容纳实现所建立的最大对象的字节大小。2)sizeof是运算符,strlen是函数。3)sizeof可以用类型做参数,strlen只能用char*做参数,且必须以“\0”结束,sizeof还可以用函数做参数。4)数组做sizeof的参数不退化,传递给strlen就退化为指针。5)大部分编译程序在编译的时候就把sizeof计算了,是类型或是变量的长度,这是sizeof可以用来定义数组维数的原因。6)strlen的结果要在运行的时候才能计算出来,用来计算字符串的长度,而不是类型占内存的大小。7)sizeof后如果类型必须加括号,如果是变量可不加,因为sizeof是个操作符不是函数。8)数组作为参数传给函数时传的是指针而不是数组,传递的是数组的首地址,编译器不知道数组的大小,如果想在函数里知道数组的大小,需要进入函数后用memcpy将数组赋值出来,长度由另一个形参传出去。9)计算结构变量大小就必须讨论数据对齐问题,为了使CPU存取的速度最快,C++在处理数据时经常把结构变量中的成员的大小按照4或8的倍数计算,这就叫数据对齐(data alignment),可能浪费一些内存,但是理论上CPU速度更快了。10)sizeof不能用于函数类型、不完全类型(如void)或位字段。
例3. int **a[3][4] 占多大字节?3*4*4=48
例4. 结论
sizeof不是函数也不是一元运算符,类似宏定义的特殊关键字,sizeof()括号内的内容在编译过程中是不被编译的,而是被替代类型,如int a=9;sizeof(a)在编译过程中替换为sizeof(int),结果为4.如果sizeof(a=6),也是一样转换为a的类型,且不被编译,所以a的值还是9。
1)unsigned影响的只是最高位bit的正负,数据长度不会变,即:sizeof(unsigned int) == sizeof(int)
2)自定义类型的sizeof取值等同于它的类型原型,如:typedef short WORD; sizeof(short) == sizeof(WORD);
3)对函数使用sizeof,在编译阶段会被函数返回值的类型取代,如:int f1(){return 0;} cout<<sizeof(f1())<<endl;//f1返回值是int,因此认为是int
4)只要是指针,大小就是4
5)数组的大小就是乘积。
例5. 如下返回结果是4,因为var[]等价于*var,已经退化为一个指针了
char var[10]; int test(char var[]) { return sizeof(var); };
例6. 如下float占4个字节,char占1个字节,int adf[3]占12字节,总共17字节,根据内存对齐原则,选择4的倍数,所以是20字节
class B{ float f;char p;int adf[3]; }; cout<<sizeof(B)<<endl;
例7. 一个空类占1个空间,单一继承的空类空间也为1,多重继承的空类空间也是1,但是虚继承涉及虚表(虚指针),另当别论。
2.4 内联函数和宏定义 (参考链接)
1.内联函数在运行时可调试,而宏定义不可以;
2.编译器会对内联函数的参数类型做安全检查或自动类型转换(同普通函数),而宏定义则不会;
3.内联函数可以访问类的成员变量,宏定义则不能;
4.在类中声明同时定义的成员函数,自动转化为内联函数。
内联函数和普通函数相比可以加快程序运行的速度,因为不需要中断调用,在编译的时候内联函数可以直接被镶嵌到目标代码中,而宏只是一个简单的替换。内联函数需要做参数类型检查。inline是指嵌入代码,就是在调用函数的地方不是跳转,而是把代码直接写到那里去,对于短小的代码来说inline是增加空间消耗换来的是效率提高,这方面和宏一样,但是inline和宏相比没有付出任何额外代价的情况下更安全。
内联函数一般在一个函数不断被重复调用或者函数只有简单几行且函数内不包含for、while、switch语句时调用。宏尽量少使用,它不是函数,只是在编译前将程序中有关字符串替换成宏体。inline是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。
关键字inline必须与函数定义体放在一起才能是函数成为内联,仅将inline放在函数声明前不起任何作用,inline void Foo(){……}
3. 指针和引用
3.1 指针基本问题
例1. 指针和引用的差别?
★ 相同点:
1. 都是地址的概念;
指针指向一块内存,它的内容是所指内存的地址;引用是某块内存的别名。
★ 区别:
1. 指针是一个实体,而引用仅是个别名;
2. 引用使用时无需解引用(*),指针需要解引用;
3. 引用只能在定义时被初始化一次,之后不可变,但是指定的对象其内容可以变;指针可重新赋值指向另一个对象;
4. 引用没有 const,指针有 const;
5. 引用不能为空,指针可以为空;
6. “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身(所指向的变量或对象的地址)的大小;
7. 指针和引用的自增(++)运算意义不一样;
8. 从内存分配上看:程序为指针变量分配内存区域,而引用不需要分配内存区域。
例2.
#include <iostream> using namespace std; int main() { int iv; //T int iv2=1024; //T int iv3=999; //T int &reiv; //F,声明引用的同时必须初始化 int &reiv2 = iv; //T int &reiv3 = iv; //T int *pi; //T,声明了一个整数指针,并没有定义指向的地址 *pi = 5; //F,整数指针pi并没有指向实际的地址,直接赋值是错误的 pi=&iv3; //T,指针pi指向iv3实际的地址 const double di; //F,const常量赋值时必须初始化 const double maxwage=10.0; //T const double *pc=&maxwage; //T,const常量指针赋值并同时初始化 }
3.2 传递动态内存
例1. 实现两数的交换,参数传递、值传递、指针传递(地址传递)、引用传递
#include <iostream> using namespace std void swap1(int p,int q){ //swap1传的是值得副本,函数体内被修改了形参p,q(实际参数的一个拷贝) int temp; //行参被修改了,但是不影响主题函数中的a和b,是局部变量,函数swap1结束时,形参p,q所在栈被删除了 temp=p; p=q; q=temp; } void swap2(int *p,int *q){ //传的是一个地址进去,在函数体内的形参*p\*q是指向实际参数a,b的两个指针。 int *temp; //新建了一个指针,没有分配内存 *temp=*p; //不是指向而是拷贝,把*p所指向的内存里的值(实参a的值)拷贝到*temp所指向的内存里,但是int *temp没有分配内存,所以系统在拷贝时临时给了个随机地址,让它存值,分配的随机地址函数结束后不收回,造成内存泄漏。 *p=*q; *q=*temp; } void swap3(int *p,int *q){ //传的是一个地址,在函数体内的形参*p,*q指向实际参数地址的两个指针 int *temp; //int *temp新建了一个指针,没有分配内存 temp=p; //这句是指向而不是拷贝,temp指向了*p所指向的地址(也就是a) p=q; //p指向了*q所指向的地址(即b q=temp; //q指向了*temp所指向的地址(即a),函数不实现两数的交换,函数体内只是指针的变化。 } void swap4(int *p,int *q){ //可以实现两数的交换,因为他修改的是指针所指向的地址的值 int temp; temp=*p; *p=*q; *q=temp; } void swap5(int &p,int &q){ //可以实现两数的交换,是一个引用传递,修改的结果直接影响实参 int temp; temp=p; p=q; q=temp; } int main(){ int a=1,b=2; //swap1(a,b); //swap2(&a,&b); //swap3(&a,&b); //swap4(&a,&b); //swap5(a,b); cout<<a <<" "<<b<<endl; }
例2. 运行如下函数,会出现什么后果?程序崩溃,因为GetMemory不能传递动态内存,main函数中的str一直都是NULL。
#include <iostream> using namespace std; void GetMemory(char *p,int num){ p = (char *)malloc(sizeof(char) * num); }; int main(){ char *str = null; GetMemory(str,100); strcpy(str,"hello"); return 0; }
问题在GetMemory函数中,函数中的*p实际上是主函数中str的一个副本,编译器总是要为函数的每个参数制作临时副本。本例中, p申请了新的内存,只是把p所指的内存地址改变了,但是str丝毫未变,因为函数GetMemory没有返回值,因此str并不指向p所申请的那段内存,所以函数GetMemory并不能输出任何东西。每执行一次GetMemory就会申请一次内存,但申请的内存却不能有效释放,结果是内存一直被独占,最终造成内存泄漏。
如果一定要用指针参数去申请内存,那么应该采用指向指针的指针,传str的地址给函数GetMemory。
...... void GetMemory(char **p, int num){ *p=(char *)malloc(sizeof(char) * num); }; int mian(){ char *str=null; GetMemory(&str,100); strcpy(str,"hello"); cout<<*str<<" "<<str<<" "<<&str<<endl; return 0; }
打印结果为 h hello 0*22f7c.str就是字符串的值,*str首字符的值,&str是字符串的地址值。如上使用的是“指向指针的指针”也可以用函数返回值来传递动态内存地址。
//其他不变 char *GetMemory(char *p,int num){ p = (char *)malloc(sizeof(char) * num); return p; }; int main(){ char *str = NULL; str = GetMemory(str,100); strcpy(str,"hello"); return 0; }
下例说明了整型变量时如何传递的,GetMemory2把v的地址穿了进来,*z是地址里的值,是v的副本,直接修改地址里的值不需要返回值,就把v给修改了,因为v所指向地址的值发生了改变。
..... void GetMemory2(int *z){ *z=5; }; int main(){ int v; GetMemory(&v); cout<< v << endl; return 0; }
例3. 如下函数有什么问题?
char *strA(){ char str[] = "hello world"; return str; }
这个str里存的地址是函数strA栈帧里“hello world”的首地址,函数调用完成,栈帧恢复到调用strA之前的状态,临时空间被重置,堆栈“回缩”,strA栈帧不再属于应该访问的范围,存于strA栈帧里的“hello world”当然也不会被访问,这个函数正确输出结果,但违背函数的栈帧机制。分配内存时有一句话“一旦使用,它即改变”。上述函数返回的是局部变量的地址,调用函数后,局部变量str已经释放了,所以返回的结果是不确定的且不安全,随时都有可能被收回。修改如下:
const char* strA(){ chat *str="hello world"; return str; }
首先,char c[]=”hello world”;是分配一个局部数组;char *c=”hello world”;是分配一个全局数组。局部数组是局部变量,它所对应的是内存中的栈,全局数组是全局变量,它所对应的是内存中的全局区域,字符串常量保存在只读的数据段,而不是像全局变量那样保存在普通数据段(静态存储区)。
例4. 如下函数运行会导致运行时错误,因为这种做法会给一个指针分配一个随意的地址,非常危险,不被允许。
int *ptr; ptr = (int)0x8000; *ptr=oxaabb;
例5. 判断
1)函数的形参在函数未调用时预分配存储空间(False,调用到实参才会分配空间)。2)若函数的定义出现在主函数之前,则可以不用再说明(False,函数需要在它被调用前声明,与main函数无关)。3)若一个函数没有return语句,则什么值也不返回(False,主函数main中可以不写return语句,因为编译器会隐式返回0)。4)函数的形参和实参的类型应该一致(True)。
3.3 函数指针
例1. 写出函数指针、函数返回指针、const指针、指向const的指针、指向const的const指针
void (*f)() void* f() const int* int* const const int* const
例2.如下数据声明都代表了什么?
float(**def)[10];//def是一个二级指针,它指向的是一个一维数组的指针,数组的元素都是float double*(*gh)[10];//gh是一个指针,指向一个一维数组,数组元素都是double* double(*f[10])();//f是一个数组,f有10个元素,元素都是函数的指针,指向的函数类型是没有参数且返回double的函数 int*((*b)[10]);//同 int*(*b)[10],一维数组的指针 Long(* fun)(int);//函数指针,指针返回值是long,所带的参数是int,如果去掉(* fun)的括号(),那么就是指针函数 Int (*(*F))(int,int))(int);//F是一个指向函数的指针,它指向一种函数(参数类型为int,返回值为int类型的函数)
3.4 指针数组和数组指针
例1. 以下程序的输出是:
#include<stdio.h> #include<iostream> using namespace std; int main(){ int v[2][10] = {{1,2,3,4,5,6,7,8,9,10},{11,12,13,14,15,16,17,18,19,20}}; int (*a)[10] = v; //数组指针 cout<<**a<<endl; cout<<**(a+1)<<endl; cout<<*(*a+1)<<endl; cout<<*(a[0]+1)<<endl; cout<<*(a[1])<<endl; return 0; }
a定义的是一个指针指向一个10个int元素的数组,即二维数组第一行数据{1,2,…,8,9,10}。a+1表明a指针向后移动1*sizeof(数组大小);a+1后共向后移动40个字节,*a+1仅针对这一行向后移动4个字节。
输出如下:1 11 2 2 11
例2. 一个指向整型数组的指针的定义为: int (*ptr)[]; int *ptr[]是指针数组,ptr[]里面存的是地址,指向位置的值就是*ptr[0]、*ptr[1]、*ptr[2],不要存*ptr[0]=5、*ptr[1]=6,因为这里面没有相应的地址; int *(ptr[])与int *ptr[]相同;int ptr[]是一个普通数组。
指针数组,是指一个数组里面装着指针;指向数组的指针,代表它是指针,指向整个数组。
例3. 用变量a给出下面的定义
int a;//一个整型数 int *a;//一个指向整型数的指针 int **a;//指向指针的指针,他指向的指针是指向一个整型数 int a[10];//一个有10个整型数的数组 int *a[10];//一个有10个指针的数组,该指针是指向一个整型数 int (*a)[10];//一个指向有10个整型数数组的指针 int (*a)(int);//一个指向函数的指针,该函数有一个整型参数并返回一个整型数 int (*a[10])(int);//一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数
例4. 写出如下程序片段的输出
int a[]={1,2,3,4,5}; int *ptr=(int*)(&a+1); printf("%d,%d",*(a+1),*(ptr-1));
*(a+1)是正常的指针运算,a指向第一个元素,a+1移动一个整型字节指向第二个元素,加上*号是取它的值。。(int*)(&a+1)表示指向a数组的第6个元素(尽管这个元素不存在),那么(ptr-1)所指向的数据就是a数组的第5个元素—5。
数组名本身就是指针,再加个&就变成了双指针,双指针就是指二维数组,加1,就是数组整体加一行。
3.5 迷途指针
迷途指针也叫悬浮指针、失控指针,是当对一个指针进行delete操作后——这样会释放他说指向的内存——并没有把它设置为空时产生的,后面如果没有重新赋值就试图再次使用该指针,引起的结果很糟糕。虽然这个指针仍然指向原来的内存区域,但编译器已经把这块区域分配给了其他的数据,所以删除指针后把它设置为空指针很有必要。
空指针和迷途指针的区别:当delete一个指针的时候,仅仅是让编译器释放内存,但指针本身依然存在,这一个迷途指针;使用语句ptr=0可以将迷途指针改为空指针,通常如果在删除一个指针后又把它删除了一次,会造成程序非常不稳定,但是如果只删除一个空指针会非常安全,使用迷途指针或空指针是非法的,而且有可能造成程序崩溃,但空指针造成的崩溃是可预料的。。
3.6 指针和句柄
句柄是一个32位的整数,实际上是windows在内存中维护的一个对象内存物理地址列表的整数索引,因为windows的内存管理经常会将当前空闲对象的内存释放掉,当需要时访问再重新提交到物理内存,所以对象的物理地址是变化的,不允许程序直接通过物理地址来访问对象。程序将想访问的对象的句柄传递给系统,系统根据句柄检索自己维护的对象列表就能知道程序想放问的对象及其物理地址了。句柄是一种指向指针的指针,windows是一个以虚拟内存为基础的操作系统,对象经常在内存中移动来满足程序需求,对象移动意味着地址变化了,windows为各应用程序腾出一些内存地址,用来专门登记各应用对象在内存中的地址变化,内存管理器把移动对象的新地址告诉告知这个句柄地址来保存,只要记住句柄地址就可以知道对象具体在哪存储。即:句柄地址(稳定)—>记载着对象在内存中的地址—>对象在内存中的地址(不稳定)—>实际对象。
windows系统用句柄标记系统资源,指针标记某个物理内存的地址,不同的概念。
3.7 this指针
this指针注意的有以下:
1)This指针本身是一个函数参数,只能在成员函数中使用,全局函数和静态函数不可用。成员函数默认第一个参数为 T* const this
2)this在成员函数的开始前构造,在成员的结束后清除。
3)this并不占用对象的空间,相当于非静态成员函数的隐含的参数。不过所有成员函数不管是不是隐含的都不会占用对象的空间,只会占用参数传递时的栈空间,或者直接占用个寄存器。
4)采用TYPE xx方式定义的话,即C中的struct,在栈里分配内存,这时this指针的值就是这块内存的地址;采用new方式创建对象的话,在堆里分配内存,new操作符通过eax返回分配的地址,谈后设置给指针变量,之后去调用构造函数,这时将这个内存块的地址传给ecx。
5)大多数编译器通过ecx寄存器传递this指针。
6)this指针只有在成员函数中才有定义,但与对象之间没有包含关系,不能通过对象使用this指针。
4. 循环、递归和概率
4.1 递归基础知识
例1. 递归函数mystrlen(char *buf,int N)是用来实现统计字符串中第一个空字符前面的字符长度。
#include<iostream> using namespace std; int mystrlen(char *buf,int N){ if(buf[0]==0||N==0) return 0; else if(N==1) return 1; int t=mystrlen(buf,N/2); //折半递归取长度 if(t<N/2) return t;//如果长度小于输入N值的一半,取当前长度 else return (t+mystrlen(buf+N/2,(N+1)/2)); } int main(){ char buf[]={'a','b','c','d','e','f','\0','x','y','z'}; int k; k=mystrlen(buf,20); cout<<k<<endl; return 0; }
例2. 棋盘上从A点到B点,走最短路径长度的路径有几条?
假设从左上A到右下B的不同走法有f(M,N)种,根据递推式f(M,N)=f(M-1,N)+f(M,N-1),等同于在当前点向下走一步和向右走一步的情况,递归终止条件为 f(M,0)==f(0,N)==0和f(1,1)==1,对于有限制的的棋盘,则可以分开讨论。
例3. F(0)=0,F(1)=1,F(n)=F(n-1)+F(n-2),求F(1025) mod 5?
对递归函数取余,先做拆项。
F(5n)=F(5n-1)+F(5n-2)=2*F(5n-2)+F(5n-3)=3*F(5n-3)+2*F(5n-4)=5*F(5n-4)+3*F(5n-5),所以F(1025) mod 5 = 3*F(1020) mod 5;依次类推,F(1020) mod 5 = 3*F(1015) mod 5;F(10) mod 5 = 3*F(5) mod 5; F(5)为5,所以最后取值0.
例4. 设计递归算法 x(x(8))需要调用几次函数x(int n).
class Program{ static void Main(string[] args){ int i; i=x(x(8)); } static int x(int n){ if(n<=3){ return 1; } else{ return x(n-2) + x(n-4) + 1; } } }
单计算x(x(8))的值是9,调用了多少次?将x(8)理解为一个二叉树,树的节点个数就是调用次数:
8—6,4—4,2,2,0—2,0,统计得调用了9次,且x(8)=9;x(x(8))=x(9),二叉树调用了 9—7,5—5,3,3,1—2,1,调用了9次;所以一共调用了18次。
4.2 典型递归/循环与数组问题
例1. 以下代码的输出结果是什么?
#include <iostream> #include <string> using namespace std; int main(){ int x=10,y=10,i; for(i=0;x>8;y=i++){ printf("%d,%d,",x--,y); } return 0; }
我做的输出结果是10,1,9,2,当然错了啊!!for循环里i=0是初始化变量,x>8是循环条件,只要x>8就执行循环,但是y=i++在第一次循环时是不执行的,只是一个递增条件,仅在第二次循环开始时才执行,所以结果应该是10,10,9,0(虽然y=i++中++的优先级高于=,但是这个等价于y=i,i++.)
a = ++i,相当于 i=i+1; a = i;
a = i++,相当于 a = i; i=i+1;
例2. 输入n,求一个n*n矩阵,规定矩阵沿45度线递增,形成一个zigzag数组(JPEG编码里取像素数据的排列顺序),注:在JPEG图形算法中首先对图像进行分块处理,一般分成互不重叠且大小一致的块,量化的结果保留了低频部分的系数,去掉了高频部分的系数,量化后的系数按zigzag扫描重新组织,然后进行哈夫曼编。
/* 得到如下样式的二维数组zigzag(JPEG编码里取像素数据的排列顺序) 0 ,1 ,5 ,6 ,14,15,27,28 2 ,4 ,7 ,13,16,26,29,42 3 ,8 ,12,17,25,30,41,43 9 ,11,18,24,31,40,44,53 10,19,23,32,39,45,52,54 20,22,23,38,46,51,55,60 21,34,37,47,50,56,59,61 35,36,48,49,57,58,63,63 */ #include <iostream> #include <stdio.h> int main(){ int N,s,i,j,squa; scanf("%d",&N); //为指向int型指针的指针分配空间,该指针指向n个型指针 int **a=(int **)malloc(N*sizeof(int)); if(a==NULL) return 0; for(i=0;i<N;i++){ if((a[i] = (int *)malloc(N*sizeof(int)))==NULL){ //对于前面的指针的每个值赋值,使其指向一个int数组,如果分配失败,则释放在它之前申请成功的空间 while(--i>=0){ free(a[i]); } free(a); return 0; } } //数组赋值 squa = N*N; for(i=0;i<N;i++){ for(j=0;j<N;j++){ s=i+j; if(s<N) a[i][j] = s*(s+1)/2 + (((i+j)%2==0)?i:j); else{ s=(N-1-i) + (N-1-j); a[i][j] = squa-s*(s+1)/2-(N-(((i+j)%2==0)?i:j); } } } //打印输出 for(i=0;i<N;i++){ for(j=0;j<N;j++) printf("%6d",a[i][j]); printf("\n"); } return 0; }
4.3 螺旋队列问题
例1. 看清以下数字排列的规律,设1点的坐标是(0,0),x方向向右为正,y方向向下为正。例如,7的坐标为(-1,-1),2的坐标为(0,1),3的坐标为(1,1),编程实现输入任意一点坐标(x,y),输出所对应的数字。
21 22 ……
20 7 8 9 10
19 6 1 2 11
18 5 4 3 12
17 16 15 14 13
这个队列是一个按顺时针方向螺旋向外扩展的,第0层为中间的1,第1层为2-9,第2层为10-25,1,9,25…是连续奇数的平方数,且第t层之内共有(2t-1)(2t-1)个数,因而第t层会从[(2t-1)(2t-1)+1]开始继续往外螺旋伸展,给定坐标(x,y),它的层数t=max(|x|,|y|),知道了层数那么就一定在第t层这个圈上,注意螺旋队列数值增长方向和坐标轴方向并不一定相同,分如下4种:
右:x==t,队列增长方向和Y轴一致,正右方向(y=0)数值为(2t-1)^2+t,所以v=(2t-1)^2+t+y.
下:y==t,队列增长方向和X轴相反,正下方向(x=0)数值为(2t-1)^2+3t,所以v=(2t-1)^2+3t-x
左:x==-t,队列增长方向和Y轴相反,正左方向(y=0)数值为(2t-1)^2+5t,所以v=(2t-1)^2+5t-y
上:y==-t,队列增长方向和X轴一致,正上方向(x=0)数值为(2t-1)^2+7t,所以v=(2t-1)^2+7t+x
右上角的队列增长不太完全符合公式,由本层的最后一个往下到下一层的第一个数。
#include <stdio.h> #include max(a,b) ((a)<(b)?(b):(a)) #define abs(a) ((a)>0?(a):(-a)) int foo(int x,int y){ int t = max(abs(x),abs(y)); int u = t + t; int v = u - 1; v = v * v + u; if(x==-t) v+=u+t-y; else if(y==-t) v+=3*u+x-t; else if(y==t) v+=t-x; else v+=y-t; return v; } int main(){ int x,y; for(y=-4;y<=4;y++){ for(x=-4;x<=4;x++){ printf("%5",foo(x,y)); } printf("\n"); } while(scanf("%d%d",&x,&y)==2) printf("%d\n",foo(x,y)); return 0; }
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9
#include <iostream> using namespace std; int a[10][10]; void Fun(int n){ int m=1,j,i; for(i=0;i<n/2;i++){ for(j=0;j<n-i;j++){ if(a[i][j]==0) a[i][j]=m++; } for(j=i+1;j<n-i;j++){ if(a[j][n-1--i]==0) a[j][n-1-i]=m++; } for(j=n-i-1;j>i;j--){ if(a[n-i-1][j]==0) a[n-i-1][j]=m++; } for(j=n-i-1;j>i;j--){ if(a[j][i]==0) a[j][i]=m++; } } if(n%2==1) a[n/2][n/2]=m; } main(void){ int n,j,i; cin>>n; for(int i=0;i<n;i++){ for(int j=0;j<n;j++){ a[i][j]=0; } } Fun(n); for(i=0;i<n;i++){ for(int j=0;j<n;j++){ cout<<a[i][j]<<" "; } cout<<endl; } }
例2. 判断非素数
int judge(int a){ int j; for(j=2;j<=aqrt(a);j++){ if(a%j==0) return 1; //非素数,退出 } return 0; }
5.STL模板与容器
标准模板库(STL,standard template library)可以方便、容易地实现搜索数据或对数据排序等一系列算法。
模板:类(及结构等各种数据类型和函数)的宏(macro),一个类的模板叫做泛型类,而一个函数的模板叫做泛型函数。
容器:可容纳一些数据的模板类,STL中有vector,set,map,multimap,deque等容器。
向量:基本数组模板,这是一个容器。
游标:是一个指针,指向STL容器中的元素,也可以指向其他的元素。
5.1 向量容器
STL是一个基于模板的容器类库,包括链表、列表、队列和堆栈,标准模板库还包括许多常用的算法,包括排序和查找。容器是包容其他对象的对象,标准模板库容器类有两种类型,分为顺序和关联,顺序容器可以提供对其成员的顺序访问和随机访问,关联容器则经过优化关键值访问它们的元素。标准模板库在不同操作系统间是可移植的,所有标准模板库容器类都在namespace std中定义。
例1. 如下有什么错误?
#include <iostream> #include <cstdlib> #include <vector> using namespace std; class CDemo{ public: CDemo():str(NULL){}; ~CDemo(){ if(str) delete[] str; }; char* str; }; int main(int argc,char** argv){ CDemo dl; dl.str=new char[32]; strcpy(dl.str, "trend micro"); vector<CDemo> *al=new vector<CDemo>(); al->push_back(dl); delete al; return EXIT_SUCCESS; }
程序退出时,重复delete同一片内存,程序崩溃。任何对象如果是通过new操作符申请了空间,必须显示的调用delete来销毁这个对象。al的声明和初始化语句“vector<CDemo> *al=new vector<CDemo>()”说明al所含元素是CDemo类型的,在执行“al->push_back(dl)”这条语句时,会调用CDemo的拷贝构造函数,即CDemo的默认拷贝构造函数(浅拷贝),这里的问题就是al中的所有CDemo元素的str成员变量没有初始化,只有一个四字节(32字节)指针空间。“al->push_back(dl)”执行完后,al里的CDemo元素与dl是不同的对象,但是al里的CDemo元素的str与dl.str指向的是同一块内存。在main函数退出时,自动释放所占内训空间,会自动调用CDemo的析构函数“~CDemo”,前面的“delete al”已经把dl.str释放了(因为al里的CDemo元素的str与dl.str指向的是同一块内存),main函数退出时,又要释放已经释放掉的dl.str内存空间,所以程序奔溃。
如果CDemo类添加一个拷贝构造函数就可以解决问题:
CDemo(const CDemo &cd){ this->str = new char[strlen(cd.str)+1]; strcpy(str,cd.str); };
5.2 泛型编程
STL是泛型编程的例子。
6.面向对象
6.1 基本概念
面向对象的三原则:封装、继承、多态。
里氏代换原则是继承复用的基石:子类型必须能够替换它们的基类型。开闭原则是面向对象设计的重要特性之一:软件对扩展应该是开放的,对修改应该是关闭的。防御式编程是一种编程技巧,与面向对象无关。防御式编程主要思想:子程序应该不因传入错误数据而被破坏,哪怕是由其他子程序产生的错误数据,这种思想是将可能出现的错误造成的影响控制在有限的范围内。
封装可以增加代码的内聚性,通过增加内聚性,进而提高可复用性和可维护性,此外还可以“信息隐藏”:把不该暴露的信息隐藏起来,如private、protected之类的关键字,通过访问控制达到信息隐藏。
C++的空类默认产生4个成员函数:默认构造函数、析构函数、拷贝构造函数和赋值函数。
6.2 类和结构
构造structure和类class:class中变量默认是private,struct中的变量默认是public,struct可以有构造函数、析构函数,之间也可以继承等等,C++中的struct其实和class意义一样,唯一不同的是struct里面默认的访问控制是public,class中默认的访问控制是private。C++中存在struct关键字的唯一意义就是为了让C程序员有个归属感,是为了让C++编译器兼容以前用C开发的项目。
6.3 成员变量
静态成员变量可以在一个类的所有实例间共享数据,如果想限制对静态成员变量的访问,则必须把它们声明为保护型或私有型,不允许用静态成员变量去存放某一个对象的数据,静态成员数据是在这个类的所有对象间共享的。如果把静态成员数据设为私有,可以通过公有静态成员函数访问。
常量必须在构造函数的初始化列表里初始化,或者将其设置为static。
6.4 构造函数和析构函数/拷贝构造函数和赋值函数
MFC类库中的CObject的析构函数是虚拟的,为什么这样设置?首先,无论析构函数是不是虚函数,派生类对象被撤销时,肯定会依次上调其基类的析构函数。CObject类用虚析构函数的原因是多态的存在。
class CBase{ public: ~CBase() {....}; ...... }; class CChild: public CBase{ public: ~CChild() {.....}; ..... }; main(){ CBase * pBase; CChild c; pBase=&c; ..... return 0; }
上例中在pBase指针被撤销时,调用的是CBase的析构函数,但如果把CBase类的析构函数改成virtual型,当pBase指针被撤销时就会先调用CChild类构造函数,再调用CBase类构造函数。如果CChild类的构造函数在堆中分配了内存,而其析构函数不是virtual型,当pBase指针被撤销时,就不会调用CChild::~CChild(),从而不会释放CChild::CChild()占据的内存,造成内存泄漏。
将Object的析构函数设为virtual型,则所有CObject类的派生类的析构函数都将自动变为virtual型,这保证了在任何情况下,不会出现由于析构函数未被调用而导致的内存泄漏,这就是MFC将CObject::~CObject()设为virtual的真正原因。
例1. 析构函数可以为virtual型,为什么构造函数不能为虚呢?
虚函数采用一种虚调用的方法,虚调用是一种可以在只有部分信息的情况下工作的机制,特别允许我们调用一个只知道接口而不知道其准确对象类型的函数,但是如果要创建一个对象,你势必要知道对象的准确类型,因此构造函数不能为虚。
例2. 不能将所有函数都设置为虚函数。
因为虚函数是有代价的,由于每个虚函数的对象都必须维护一个v表,因此在使用虚函数的时候都会产生一个系统开销,如果仅是一个很小的类,且不想派生其他类,则没必要使用虚函数。
析构函数可以使内联函数。
例3. 编写类String的构造函数、析构函数和赋值函数。类String的原型如下:
class String{ public: //普通构造函数 String(const char *str = NULL); //拷贝构造函数 String(const String &other); //析构函数 ~String(void); //赋值函数 String & operate=(const String &other); private: //用于保存字符串 char *m_data; };
--------------额,断网了,后面就不能用插件插入代码了---------------
1)String的析构函数
定义析构函数是为了防止内存泄漏,当一个String对象超出它的作用域时,这个析构函数就会释放它所占的内存。
String::~String(void){
delete [] m_data;
}
2)String的构造函数
构造函数首先分配足量的内存,再把这个字符串常量复制到这块内存。
String::String(const char *str){
if(str==NULL) { m_data=new char[1];*m_data=’\0’;}
else{ int length=strlen(str); m_data=new char[length+1]; strcpy(m_data,str); }
}
3)String的拷贝构造函数
所有需要分配系统资源的用户定义类型都需要一个拷贝构造函数,这样就可以声明: Mystring s1(“hello”); Mystring s2=s1;
拷贝构造函数还可以帮我们在函数调用中以传值的方式传递一个Mystring参数,并且在当一个函数以值的形式返回Mystring对象时实现“返回时复制”。
String::String(const String &other){
int length=strlen(other.m_data);
m_data=new char[length+1];
strcpy(m_data,other.m_data);
}
一般情况下,默认的类譬如string和vector等都可以不用拷贝构造函数,因为这些都是成熟的类有合适的赋值操作,可以使用默认的拷贝构造函数,构造拷贝构造函数一般是为了避免“浅拷贝”问题。
4)String的赋值函数
赋值函数可以实现字符串的传值活动:Mystring s1(hello); Mystring s2;s1=s2;
6.5 多态
一个接口,多种方法~允许将子类类型的指针赋值给父类类型的指针,多态在C++中是通过虚函数实现的。虚函数就是允许被其子类重新定义的成员函数,子类重新定义父类虚函数的做法叫“覆盖”或者“重写”。
覆盖是指子类重新定义父类的虚函数,重写的函数必要要有一致的参数表和返回值,当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态地调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数的地址无法给出),因此,这样的函数地址是在运行期绑定的(晚绑定)。
重载是指允许存在多个同名函数,而这些函数的参数表不同(参数个数、类型),重载的实现是编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数,如编译器会将function func(p:integer):integer和function func(p:string):integer修饰为int_func和str_func,对于这两个函数的调用在编译期间就已经确定了,是静态的,他们的地址在编译期就绑定了(早绑定)。重载只是一种语言特性,与面向对象和多态都没有关系。
“如果它不是晚绑定,它就不是多态”。
多态的作用:封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了代码重用,而多态则是为了接口重用。
6.6 友元
类具有封装和信息隐藏的特性,只有类的成员函数才能访问类的私有成员,程序中的其他函数是无法访问的;非成员函数可以访问类中的公有成员,但是如果讲数据成员都定义为共有的,破坏了隐藏的特性,另外,特别是在对某些成员函数多次调用时,由于参数传递、类型检查和安全性检查等都需要时间开销,而影响程序的运行效率。友元就是为了解决这个的,是一种定义在类外部的普通函数,但需要在类体内进行说明,为了与该类的成员函数加以区别,在说明时前面加以关键字friend,友元不是成员函数,但可以访问类中的私有成员,友元的作用在于提高程序的运行效率,但是它破坏了类的封装性和隐藏性,使得非成员函数可以访问类的私有成员。
友元函数、友元类。
7. 继承与接口
7.1 覆盖 略
7.2 私有继承
派生类的3种继承方式:
1.公有继承方式 基类的公有成员和保护成员对派生类可见,基类的私有成员不可见,即在公有继承时,派生类的对象可以访问基类中的公有成员,派生类的成员函数可以访问基类中的公有成员和保护成员。
2.私有继承方式 基类的成员只能由直接派生类访问,无法再往下继承。
3.保护继承方式 和私有继承相同,唯一不同的就是对派生类的成员,基类成员对其对象的可见性与一般类及其对象的可见性相同,公有成员可见,其他成员不可见。
7.3 多重继承
1)多重继承使用不慎会出现菱形继承的现象,可以使用virtual继承等解决。 2)多重继承在面向对象理论中并非必要的,因为它不提供新的语义,可以通过单继承与复合结构来取代,java则放弃了多重继承,使用interface取代,多重继承是把双刃剑。 3)。。。
7.4 纯虚函数
虚函数指针或虚指针是一个虚函数的实现细节,带有虚函数的类中的每一个对象都有一个虚指针指向该类的虚函数表。每个虚函数都在vtable中占了一个表项,保存着一条跳转到它的入口地址的指令(实际上是保存了它的入口地址),当一个包含虚函数的对象被创建的时候,它在头部附加一个指针,指向vtable中相应的位置。调用虚函数的时候,不管你是用什么指针调用的,它先根据vtable找到入口地址再执行,从而实现了“动态联编”。而不像普通函数那样简单地跳转到一个固定地址。
例1. C++一般使用抽象类或者构造函数被声明为private来组织一个类被实例化;当需要组织编译器生成默认的拷贝构造的时候需要将构造函数声明为private;如果我写了一个构造函数,编译器还是会生成拷贝构造函数的(浅拷贝)。
8.位运算与嵌入式编程
8.1 位制转换 略
8.2 嵌入式编程 volatile
8.3 static
C语言中,static关键字的作用:1)函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值。2)在模块内的static全局变量可以被模块内所有函数访问,但不能被模块外其他函数的访问。3)在模块内的static函数只可被这一模块内的其他函数调用,这个函数的使用范围被限制在声明它的模块内。4)在类中的static成员变量是属于整个类所拥有,对类的所有对象只有一份拷贝。5)在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。