北京大学C++程序设计课程
1. 函数指针
1.1 基本概念
程序运行期间,每个函数都会占用一段连续的内存空间。而函数名就是该函数所占内存区域的起始地址(也称“入口地址”)。 我们可以将函数的入口地址赋给一个指针变量,使该指针变量指向该函数。然后通过指针变量就可以调用这个函数。这种指向函数的指针变量称为“函数指针”。
定义形式:类型名 (* 指针变量名)(参数类型1, 参数类型2,...); 类型名是返回的数据类型
例如: int (*pf)(int ,char);
使用方法:用一个原型匹配的函数的名字给一个函数指针赋值,要通过函数指针调用它所指向的函数,写法为: 函数指针名(实参表);
1.2 用例- 函数指针和qsort库函数
# include<stdio.h> # include<stdlib.h> // 实际定义的函数-void function_name(int, int) void PrintMin(int a, int b){ if(a<b) printf("%d\n", a); else printf("%d\n", b); } int main(){ // 定义函数指针:类型名(* 指针变量名)(参数类型1,参数类型2,...) void (* pf)(int , int); int x=4, y=5; // 用类型相同的函数(void name(int,int))给函数指针赋值 pf = PrintMin; // 要通过函数指针调用它指向的函数,写法为 -> 函数指针名(实参表) pf(x, y); return 0; }
那为什么不直接用函数,还要多此一举的用函数指针呢?函数指针的优点在哪里呢?接下来用c语言中的qsort,展示函数指针的用法。
void qsort (void* base, size_t num, size_t size, int (*compar)(const void*,const void*));
base
: Pointer to the first object of the array to be sorted, converted to a void*.num
: Number of elements in the array pointed to by base.
size_t is an unsigned integral type.size
: Size in bytes of each element in the array.
size_t is an unsigned integral type.compar
: Pointer to a function that compares two elements.
This function is called repeatedly by qsort to compare two elements. It shall follow the following prototype:
int compar (const void* p1, const void* p2);
上述的解释来自于官网的文档,我们会发现 compar
其实是一个函数指针,qsort函数在执行期间,会通过compar
指针调用 “比较函数”,调用时将要比较的两个元素的地址传给“比较函数”,然后根据“比较函数”返回值判断两个元素哪个更应该排在前面。
比较函数接受形式为,int 函数名(const void * elem1, const void * elem2),它是由程序员自己编写的,非常的灵活。
/* 下面的程序,功能是调用qsort库函数,将一个unsigned int数组按照个 位数从小到大进行排序。 比如 8,23,15三个数,按个位数从小到大排 序,就应该是 23,15,8 */ int MyCompare(const void* elem1, const void* elem2){ // void *不占字节,必须告诉编译器它的具体类型,才能在后面编译通过 unsigned int * p1, * p2; // 转换成一个已知的类型,编译器才能通过; p1 = (unsigned int*) elem1; // *elem1 是非法的 p2 = (unsigned int*) elem2; return (*p1 % 10) - (*p2 % 10); //return (*(unsigned int*)elem1%10) - (*(unsigned int*)elem2%10); } #define NUM 5 int main(){ unsigned int array[NUM] = {8,123,11,10,4}; qsort(array, NUM, sizeof(unsigned int), MyCompare); for(int i=0; i<NUM; i++){ printf("%d\n", array[i]); } return 0; }
我们有注意到,函数指针很类似于虚函数,具体在使用的时候才知道它到底要干什么,给了我们很大的自由性。未知的类型,都写成了void*。在c++中sort函数也有类型的用法,参考我的博客 c++的比较器
int compareMyType (const void * a, const void * b) { if ( *(MyType*)a < *(MyType*)b ) return -1; if ( *(MyType*)a == *(MyType*)b ) return 0; if ( *(MyType*)a > *(MyType*)b ) return 1; }
1.3 扩展
在网上查阅资料(引用 博客 ),看到函数指针的好处有如下:
- 便于分层设计:函数指针是引用,是间接层,或曰隔离层。它输出到上层,给上层用户用。函数实体是实现,在下层,给开发者用,实现者(软件工程师)关注。这就是简单的分层的概念了。上层用户想让一个函数所做的东西会变化时,我们只需要改变底层实现,并用函数指针指向新的实现就行了。
再精炼一下分层:分层的核心是对接口进行设计和实现。函数指针的作用就是提供不同实现的统一接口。 - 利于系统抽象:只有存在多个类似的实体需要模拟、操作或控制时(这种情况很多)才需要抽象。多个类似的实体就是对象,抽象的结果就是类。在C里边,可以用函数指针数组完成这种抽象。如, fopen 就是一个例子。他可以打开文件。C里面将磁盘文件、串口、USB等诸多设备抽象为文件。
- 降低耦合度以及使接口与实现分开。
#include<stdio.h> // 程序员具体开发,加减乘除函数,预留出接口 float add(float x,float y) {return (x+y);} float sub(float x,float y) {return (x-y);} float mul(float x,float y) {return (x*y);} float div(float x,float y) {return (x/y);} // 用户只关心result函数 float result(float x,float y,float(*pf)(float,float)) { float s; s=(*pf)(x,y); return s; } void main() { float a,b,s; char op; printf(""please select your operation (input +,-,*or/)\n"); scanf("%c",&op); printf("please input the two operand\n"); scanf("%f %f",&a,&b); switch(op) { case '+':s=result(a,b,add);break; case '-':s=result(a,b,sub);break; case '*':s=result(a,b,mul);break; case '/':s=result(a,b,div);break; } printf("the operation is :%f%c%f=%f\n",a,op,b,s); }
2. 命令行参数
2.1 什么是命令行参数
跟在可执行文件名后面的那些 字符串,称为“命令行参数”。命令行参数 可以有多个,以空格分隔。
2.2 如何在程序中获得命令行参数呢?
int main(int argc, char * argv[]) { ...... }
argc: 代表启动程序时,命令行参数的个数。C/C++语言规定,可 执行程序程序本身的文件名,也算一个命令行参数,因此,argc的值 至少是1。
argv: 指针数组,类型为char**,其中的每个元素都是一个char类型的指针,该指针指向一个字符串,这个字符串里就存放着命令行参数。
例如,argv[0]指向的字符串就是第一个命令行参数,即可执行程序的文件名,argv[1]指向第二个命令行参数,argv[2]指向第三个命令行参数......。
#include <stdio.h> int main(int argc, char * argv[]) { for(int i = 0;i < argc; i ++ ) printf( "%s\n",argv[i]); return 0; }
将上面的程序编译成sample.exe,然后在控制台窗口敲: sample para1 para2 s.txt 5 “hello world”,会得到:
sample
para1
para2
s.txt
5
hello world
3. 位运算
3.1 基本操作介绍
按位与(&)
定义:只有对应的两个二进位均为1时,结果的对 应二进制位才为1,否则为0。
应用:通常用来将某变量中的某些位清0且同时保留其他位不变。也可以用来获取某变量中的某一位。
例如,如果需要将int型变量n的低8位全置成0,而其余位 不变,则可以执行:
n = n & 0xffffff00;按位或(|)
定义:只有对应的两个二进位都为0时,结果的对应 二进制位才是0,否则为1。
应用:按位或运算通常用来将某变量中的某些位置1且保留其他位不变。
例如,如果需要将int型变量n的低8位全置成1,而其余位不变,则可以执行:
n |= 0xff;
0xff: 1111 1111按位异或(^)
定义:只有对应的两个二进位不相同时,结果的对应二进制 位才是1,否则为0。
性质:异或其实就是无进位相加;满***换律和结合律;
0^N=N; N^N=0; (a^b)^c = a^(b^c); a^b=b^a
应用:可以实现无中间变量的交换两个数;统计一个数出现奇数次和偶数次按位非(~)
定义:其功能是将操作数中的二进制位0变成1,1变成0。左移(<<)
定义:将a各二进位全部左移b位后得到的值。左移时,高位丢弃,低位补0。a的值不因运算而改变。
结论:实际上,左移1位,就等于是乘以2,左移n位,就等于是乘以2ˆn。而左移操作比乘法操作快得多。右移(>>)
定义:将a各二进位全部右移b位后得到的值。右移时,移出最右边的位就被丢弃。并且大多数C/C++编译器规定,如果原符号位为1,则右移时高位就补充1,原符号位为0,则右移时高位就补充0。
结论: 实际上,右移n位,就相当于左操作数除以2ˆn,并且将结果往小里取整。
3.2 应用:不用中间变量,交换两个数的值
int a=甲, int b=乙 a = a^b; // a=甲^乙 b=乙 b = a^b; // a=甲^乙 b=甲^乙^乙=甲 a = a^b; // a = 甲^乙^甲 = 甲^甲^乙=乙
3.3 应用:求一个数的二进制的第n位
( a >> n ) & 1
想求a的二进制的第n位Bn,a>>n,将第n位右移动n位,此刻Bn到第一位上,再&1,就是将其他位清零,只保留第一位,也就是Bn位。
4. “引用”的概念和应用
4.1 引用的概念
某个变量的引用,等价于这个变量,相当于该变量的一个别名。
下面的写法定义了一个引用,并将其初始化为引用某个变量。
类型名 & 引用名 = 某变量名;
int n = 4; int & r = n; // r引用了 n, r的类型是 int & r = 4; cout << r; //输出 4 cout << n; //输出 4 n = 5; cout << r; //输出5,r和n的内存地址都是一样的
定义引用时一定要将其初始化成引用某个变量。
引用只是别名,不是实体类型(也就是说c++编译器不为引用单独分配内存空间),因此不存在void类型的引用,和不初始化的引用。
int a = 5; void & r1 = a; // 没有空类型的引用 int & r2; // 引用必须初始化 输出: error: cannot form a reference to 'void' error: declaration of reference variable 'r2' requires an initializer
初始化后,它就一直引用该变量,不会再引用别的变量了。
引用只是别名,不是实体类型(也就是说c++编译器不为引用单独分配内存空间),因此不能再赋值成别人的引用
int a = 6; int b = 7; int & r = a; int & r = b;// error:不能再成为b的引用
引用只能引用变量,不能引用常量和表达式
在c++底层中,引用是通过指针实现的。也就是说,在实现层面上,引用就是指针。常量在常量表中不能修改,表达式根本就没有内存地址,会被编译器翻译成一些列的操作。
int n = 4; int & r = n * 5; // error: 不能引用常量和表达式
4.2 引用在函数中的应用-引用传递(引用作为参数)
在c语言中,我们知道函数中变量的生存周期和作用域,一个交换两个变量值的函数
void swap(int a, int b){ int temp = b; b = a; a = temp; } int main(){ int a = 10; int b = 100; swap(a,b);// 不起作用,swap函数是参数传递,实参的值没有改变 }
所以在c语言中,要用指针传递的函数,可以修改两个变量的值
void swap_c(int* a, int* b){ int temp = *a; *a = *b; *b = temp; } int main(){ // @ c里面的交换函数, 很丑陋,函数题一堆*,调用也要用& int arr[3] = {1, 2 ,3}; swap_c(&arr[0], &arr[1]); cout << arr[0] << " " << arr[1] << endl; }
有了c++的引用,就变得非常的干净优雅
void swap_cpp(int & a, int & b){ int temp = a; a = b; b = temp; } int main(){ // c++ 里面的交换函数, 借助引用,直接交换两个变量 int arr1[3] = {1, 2 ,3}; swap_cpp(arr1[0], arr1[1]); cout << arr1[0] << " " << arr1[1] << endl; }
小结一下:使用引用传递的函数有两个好处:
(1)因为函数形参和实参是同一个对象,也就不存在对象复制,避免了对象的开销。
(2)可以在修改形参的同时对实参的修改。
当然了,为了避免函数对原来实参的意外修改我们可以 用const 对引用加以修饰 也就是传常引用。传常引用有两个优势
(1)因为不存在拷贝构造所以,可以提高c++程序的执行效率
(2)避免对象切割问题。关于这点的详细讨论可以看effective c++ 。
4.3 引用在函数中的应用-引用作为函数的返回值
int n = 4; int & SetValue() { return n; } int main() { SetValue() = 40; cout << n; return 0; } //输出: 40
Todo:暂时不太懂,以后会讲
4.4 加const关键字的常引用和其他的注意⚠️
定义引用时,前面加const关键字,即为“常引用”,r 的类型是 const int &
int n; const int & r = n;
1.不能通过常引用去修改其引用的内容:
int n = 100; const int & r = n; r = 200; //编译错 n = 300; // 没问题
这里要注意的是,变量n还是可以改变的
2.const T & 和T & 是不同的类型!!!
T & 类型的引用或T类型的变量可以用来初始化 const T & 类型的引用。
const T 类型的常变量和const T & 类型的引用则不能用来初始化T &类型的引用,除非进行强制类型转换。
int n = 8; const int & r1 = n; int & r2 = r1;// 错误❌:非常引用不能通过常引用初始化,想象成低级不兼容高级 int n = 8; int & r1 = n; const int & r2 = r1;// 正确✅:常引用可以通过非常引用初始化,高低兼容低级
本小节部分内容参考:
CSDN博主「静远1175」的原创文章
5. “const”关键字的用法
参考高质量c++代码(林锐著):const是constant的缩写,被const修饰的东西都收到c/c++语言实现的静态类型安全检查机制的强制保护,可以预防额外修改,能提高程序的健壮性。
const不仅仅用于定义符合常量,凡是需要编译器帮助我们预防无意修改数据的地方,都可以使用const,比如const数据成员,const成员函数,const返回类型,const参数等。
5.1 定义常量
const int MAX_VAL = 23; const string SCHOOL_NAME = “TU Berlin University” ;
5.2 定义常量指针
⚠️不可通过常量指针修改其指向的内容
int n,m; const int * p = & n; * p = 5; //编译出错 n = 4; //ok p = &m; //ok, 常量指针的指向可以变化
⚠️不能把常量指针赋值给非常量指针,反过来可以
const int * p1; int * p2; p1 = p2; //ok p2 = p1; //error 常量指针赋值给非常量,不可以,存在安全风险,可以通过p2修改p1指向的值 p2 = (int * ) p1; //ok,强制类型转换
⚠️函数参数为常量指针时,可避免函数内部不小心改变参数指针所指地方的内容
void MyPrintf( const char * p ) { strcpy( p,"this"); //编译出错,strcpy试图修改p的内容! printf("%s",p); //ok }
5.3 定义常量引用
略,之前讨论过
6. 动态内存分配
这块讲的很浅,以后要补充大量知识。c++的内存管理是非常重要的部分!
6.1 第一种用法,分配一个变量:
P = new T;
T是任意类型名,P是类型为T * 的指针。
动态分配出一片大小为 sizeof(T)
字节的内存空间,并且将该 内存空间的起始地址赋值给P。比如:
int * pn; pn = new int; * pn = 5;
用“new”动态分配的内存空间,一定要用“delete”运算符进行释放,
delete 指针;//该指针必须指向new出来的空间 int * p = new int; * p = 5; delete p; delete p; //导致异常,一片空间不能被delete多次
6.2 第二种用法,分配一个数组:
P = new T[N]; T :任意类型名 P :类型为T * 的指针 N :要分配的数组元素的个数,可以是整型表达式
动态分配出一片大小为 sizeof(T)字节的内存空间,并且将该内存空间的起始地址赋值给P。
示例:
int * pn; int i = 5; pn = new int[i * 20]; pn[0] = 20; pn[100] = 30; //编译没问题。运行时导致数组越界
用“delete”释放动态分配的数组,要加“[]”
delete [ ] 指针;//该指针必须指向new出来的数组 int * p = new int[20]; p[0] = 1; delete [ ] p;
7. 内联函数和重载函数
7.1 什么是内联函数(inline关键字)
一个普通函数的调用过程是:把函数参数压入栈,返回地址也入栈,执行完毕后取出返回地址,再跳转到返回地址去。
函数调用是有时间开销的。如果函数本身只有几条语句,执行非常快,而且函数被反复执行很多次,相比之下调用函数所产生的这个开销就会显得比较大。
为了减少函数调用的开销,引入了内联函数机制。编译器处理对内联函数的调用语句时,是将整个函数的代码插入到调用语句处,而不会产生调用函数的语句。
Tip: 只有当函数只有 10 行甚至更少时才将其定义为内联函数.
inline int Max(int a,int b) { if( a > b) return a; return b; } // 如果有语句 k = Max(n1,n2),编译器会翻译成如下并插入到当前行 if(n1>n2) tmp = n1; else tmp = n2; k = tmp;
7.2 慎用内联函数
Tip: 只有当函数只有 10 行甚至更少时才将其定义为内联函数.
Tip: 复杂的内联函数的定义, 应放在后缀名为 -inl.h 的头文件中.
7.3 函数重载
一个或多个函数,名字相同,然而参数个数或参数类型不相同,这叫做函数的重载。
//以下三个函数是重载关系: int Max(double f1,double f2) { } int Max(int n1,int n2) { } int Max(int n1,int n2,int n3) { }
如果仅仅是返回类型不同,不是重载!
int Max(int a, int b){ return a>b:a?b; } double Max(int a, int b){ return (double) a>b:a?b; } // error: functions that differ only in their return type cannot be overloaded
- 函数重载使得函数命名变得简单。
- 编译器根据调用语句的中的实参的个数和类型判断应该调用哪个函数。
(1) int Max(double f1,double f2) {} (2) int Max(int n1,int n2) {} (3) int Max(int n1,int n2,int n3) {} Max(3.4,2.5); //调用 (1) Max(2,4); //调用 (2) Max(1,2,3); //调用 (3) Max(3,2.4); //error,二义性
8. 函数缺省参数
C++中,定义函数的时候可以让最右边的连续若干个参数有缺省值,那么调用函数的时候,若相应位置不写参数,参数就是缺省值。
void func( int x1, int x2 = 2, int x3 = 3) { } func(10 ) ; //等效于 func(10,2,3) func(10,8) ; //等效于 func(10,8,3) func(10, , 8) ; //不行,只能最右边的连续若干个参数缺省
- 函数参数可缺省的目的在于提高程序的可扩充性。
- 即如果某个写好的函数要添加新的参数,而原先那些调用该函数的语句,未必需要使用新增的参数,那么为了避免对原先那些函数调用语句的修改,就可以使用缺省参数。
例子:比如某个画图函数默认画黑色,现在新增一个参数color可以修改颜色,但其实以前写的大部分函数都是只需要黑色就可以,这时候可以写成 drawSomething(..., color=black)
,就不用依次修改以前的函数参数,如果某个图想画红色drawSomething(..., color=red)