本文是“北京大学—程序设计实习[C++]”公开课的听课笔记,配套教材是任课老师自己编写的《新标准C++程序设计教程》,博主是C++入门菜鸟,故整理并记录在博客里,方便后续查看。

函数指针

程序运行期间,每个函数都会占用一段连续的内存空间。函数名就是该函数所占内存区域的起始地址(也称入口地址)。可以将函数的入口地址赋给一个指针变量,使该指针变量指向该函数。通过指针变量调用这个函数,这种指向函数的指针变量称为“函数指针”。

函数指针示意图

定义形式

类型名(*指针变量名)(参数类型1,参数类型2,...)

例如:int(*pf)(int, char);

上面表示pf是一个函数指针,函数指针的返回值和参数类型应与指向函数一致:他所指向的函数的返回值类型应该是int,该函数有两个参数,第一个是int类型,第二个时char类型。

使用方法

可以用一个原型匹配的函数的名字,给一个函数指针赋值,通过函数指针调用它所指向的函数,写法为:

函数指针名(实参表)

例如:pf(x, y);

# include <stdio.h>
void PrintMin(int a, int b) {
	if (a < b)
		printf("%d", a);
	else
		printf("%d", b);
}

int main() {
	void(*pf)(int, int); //定义函数指针
	int x = 4, y = 5;
	pf = PrintMin; //给函数指针赋值
	pf(x, y); //使用函数指针
	return 0;
}

函数指针和qsort库函数

C语言快速排序库函数:qsort库函数,可以对任意类型的数组进行排序,定义为:

void qsort(void *base, int nelem, unsigned int width, int(*pfCompare)(const void *,const void *));
  1. void *base:数组起始位置
  2. int nelem:数组元素的个数
  3. unsigned int width:每个元素的大小(由此可以算出每个元素的地址)
  4. int(*pfCompare)(const void *,const void *):元素谁在前谁在后的规则

函数指针:pfCompare,它指向一个“比较函数”,该比较函数应为以下形式:

int 函数名(const void *elem1, const void *elem2);

排序就是一个不断比较并交换位置的过程。

qsort函数在执行时,会通过pfCompare指针调用“比较函数“,调用时将要比较的两个元素的地址传给”比较函数“,然后根据“比较函数”返回值判断两个元素哪个更应该排在前面。

比较函数规则:

  1. 如果* elem1应该排在* elem2前面,则函数返回值是负整数
  2. 如果* elem1* elem2哪个在前面都可以,则函数返回0
  3. 如果* elem1应该排在* elem2后面,则函数返回值是正整数

实例1:

下面的程序,功能是调用去sort库函数,将一个unsigned int数组按照个位数从小到大进行排序,比如8,23,15三个数,按个位数从小到大排序,应该是23,15,8;

分析:

需要自己定义一个比较规则,编写比较函数

int myCompare(const void* elem1, const void* elem2)

其中需要注意的是,* elem1void *类型的指针,编译器不知道elem1指向的元素是多少字节,所以无法编译。这里加了个(usigned int *)的强制转换。

程序:

#include <stdio.h>
#include <stdlib.h>

int myCompare(const void* elem1, const void* elem2) {
	unsigned int* p1, * p2;
	p1 = (unsigned int*)elem1; //"* elem1"非法
	p2 = (unsigned int*)elem2;
	return (*p1 % 10) - (*p2 % 10);
}

#define NUM 5
int main() {
	unsigned int an[NUM] = { 8,123,11,10,4 };
	qsort(an, NUM, sizeof(unsigned int), myCompare);
	for (int i = 0; i < NUM; i++) {
		printf("%d ", an[i]);
	}
	return 0;
}//输出结果:10 11 123 4 8

命令行参数

命令行方式运行程序

notepad sample.txt

命令行参数

命令行参数:用户在CMD窗口输入可执行文件名的方式启动程序时,跟在可执行文件名后面的那些字符串。

命令行参数可以有多个,以空格分隔,比如:

copy file1.txt file2.txt

copyfile1.txtfile2.txt就是命令行参数。

如果需要获取参数,需要将程序的main函数修改:

int main(int argc, char * argv[])
{
	.......
}

其中:

argc:代表启动程序时,命令行参数的个数,C/C++语言规定,可执行程序本身的文件名也算一个命令行参数,因此,argc的值至少是1。

argv[]:指针数组,其中每个元素都是一个char*类型的指针,该指针指向一个字符串,这个字符串就存放在命令行参数。

例如:argv[0]指向的字符串就是第一个命令行参数,即可执行程序的文件名,argv[1]指向第二个命令行参数,argv[2]指向第三个命令行参数……。

例程3:

将下面的程序编译成1.3sample.exe,然后在控制台窗口运行。

#include <stdio.h>
int main(int argc, char* argv[]) {
	for (int i = 0; i < argc; i++) {
		printf("%s\n", argv[i]);
	}
	return 0;
}

控制台执行:

1.3sample.exe para1 para2 s.txt 5 "hello world"

输出结果

1.3sample.exe
para1
para2
s.txt
5
hello world

命令行参数用空格分隔,如果参数内部有空格,可以用“ ”分隔。

位运算(P28)

对整数类型(int, char, long等)变量中的某一位(bit),或者若干位进行操作。比如:

  1. 判断某一位是否为1;
  2. 只改变其中某一位,而保持其他位都不变;

C/C++语言提供了六种位运算符操作:

符号 定义 说明
& 按位与,双目 1&1=1, 其余为0
| 按位或,双目 0|0=0,其余为1
^ 按位异或,双目 10=01=1, 其余为0
~ 按位非/取反,单目 0=1,1=0
<< 左移,双目
>> 右移,双目

注:单目运算符只对一个操作数进行逻辑运算。双目运算符对两个操作数进行逻辑运算。

按位与“&”

只有对应的两个二进位均为1时,结果的对应二进制位才为1,否则为0。

例如:21 & 18 = 16;

作用:

  1. 通常用来将变量中的某些位清0,同时保留其他位不变。

  2. 获取某变量中的某一位

例如,需要将int型变量n的低8位全置成0,而其余位不变,则可以执行:

n = n & 0xffffff00

低8位是0,高24位都是1

按位与的结果,低8位都是0,高24位都不变,也可以写成:

n &= 0xffffff00

如果n是short类型的,则只需执行

n &= 0xff00

例题:

如何判断一个int型变量n的第7位(从右往左,从0开始数)是否是1?

只需要看表达式n & 0x80的值是否等于0x80即可,其中0x80: 1000 0000

按位或“|”

二进位都为0时,结果对应的二进位才是0,否则为1。

例如:21|18=23;

作用:

将某变量中的某些位置1且保留其他位不变。

例如,如果将int型变量n的低8位全置成1,而其余位不变,则可以执行:

n |= 0xff;

0xff:1111 1111

按位异或“^”

两个二进制位不相同时,结果位1,否则为0。

例如:21^18 = 7;

作用:

将某变量中的某些位取反,且保留其他位不变。

例如,将int类型变量n的低8位取反,而其余位不变,则:

n ^= 0xff;

0xff:1111 1111

异或运算的特点:

如果ab=c,则cb=a,c^a=b。(穷举法可证),此规律可以用来进行最简单的加密和解密。

作用2:

实现不通过临时变量,交换两个变量的值:

init a = 5, b =7;
a = a ^ b;
b = b ^ a;
a = a ^ b;

按位非“~”

单目运算符,取反。

例如:~21的值是整型数:0xffffffea;

21:0000 0000 0000 0000 0000 0000 0001 0101

~21:1111 1111 1111 1111 1111 1111 1110 1010

左移运算符“<<”

表达式:a<<b 的值是:将a各二进位全部左移b位后得到的值。左移时,高位丢弃,低位补0。

例如:9<<4结果的十进制是144。

实际上,左移1位,就等与x2,左移n位,就等于是乘以2n2^n,且左移操作比乘法操作快得多。

右移运算符“>>”

表达式:a>>b,将a的二进位右移b位,右移时,移出最右边的位被丢弃。

对于有符号数,如long,int,short,char类型变量,右移时,符号位(最高位)将一起移动,并却,大多数C/C++编译器规定,如果原符号位为1,则右移时高位就补充1,原符号位为0,高位补充0。

实际上,右移n位,相当于左操作数除以2n2^n,并且将结果往小里取整。

-25 >> 4 = -2

-2 >> 4 = -1

18 >> 4 = 1

例程:

#include <stdio.h>
int main() {
	int n1 = 15;
	short n2 = -15;
	unsigned short n3 = 0xffe0;
	char c = 15;
	n1 = n1 >> 2;
	n2 >>= 3;
	n3 >>= 4;
	c >>= 3;
	printf("n1=%d, n2=%x, n3=%x, c=%x", n1, n2, n3, c);
}//n1=3, n2=fffffffe, n3=ffe, c=1

结果分析:

其中int类型的变量有32位,占用4个字节,short有16位,unsigned short有16位,char类型有8位,占用1个字节。有符号的数,左边补进来的为符号数。

n1:0000 0000 0000 0000 0000 0000 0000 1111

n1 >>= 2:变成3

n2:1111 1111 1111 0001

n2 >>= 3:变成 fffffffe,即-2

n3:1111 1111 1110 0000

n3 >>= 4:变成ffe(0000 1111 1111 1110)

c:0000 1111

c >>= 3:变成1

思考题:

有两个int型的变量a和n(0<=n<=31),要求写一个表达式,使该表达式的值和a的第n位相同。

(a >> n)& 1

如果n不为31,另一种答案:

(a &(1<< n))>> n

“引用”的概念和应用

引用的概念(P62)

某个变量的引用,等价于这个变量,相当于该变量的一个别名。

定义方法:类型名 & 引用名 = 某变量名;

int n = 4;
int & r = n;//r引用了n,r的类型是 int &

例程:

int n = 7;
int & r = n;
r = 4;
cout << r;//输出4
cout << n;//输出4
n = 5;
cout << r;//输出5
  • 定义引用时,一定要将其初始化成引用某个变量。

  • 初始化后,他就一直引用该变量,不会再引用别的变量了。

  • 引用只能引用变量,不能引用常量和表达式。

例题:

#include <stdio.h>
#include <iostream>

using namespace std;

int main() {
	double a = 4, b = 5;
	double& r1 = a;
	double& r2 = r1;//r2也引用a
	r2 = 10;
	cout << a << endl;//输出10
	r1 = b;//赋值语句,r1并没有引用b
	cout << a << endl;//输出5
}

引用应用的简单示例

c语言中,如何编写交换两个整型变量值的函数?

使用指针进行交换整型变量

#include <stdio.h>
#include <iostream>

using namespace std;

void swap(int* a, int* b) {
	int tmp;
	tmp = *a;
	*a = *b;
	*b = tmp;
}

int main() {
	int n1 = 10, n2 = 5;
	cout << "before swap:" << n1 << "," << n2 << endl;
	swap(&n1, &n2);
	cout << "after swap:" << n1 << "," << n2 << endl;
}

有了C++的引用:

void swap2(int& a, int& b) {
	int tmp;
	tmp = a;
	a = b;
	b = tmp;
}
int n1, n2;
swap(n1, n2);//n1,n2的值被交换

a,b分别引用了n1,n2,所以,函数里面是等价的。

引用作为函数的返回值(P63)

返回值是引用时,可以对其赋值,返回值为n,赋值

int n = 4;
int& SetValue(){
    return n;
}

int main(){
    SetValue() = 40;
    cout << n;
    return 0;
}//输出:40

常引用(P65)

定义引用时,前面加const关键字,即为“常引用”

int n;
const int& r = n;

r 的类型是const int &

不能通过常引用去修改其引用的内容:

int n = 100;
const int& r = n;
r = 200;//编译错
n = 300;//编译没问题

常引用和非常引用的转换

const T&和T&是不同的类型!

T&类型的引用或T类型的变量可以用来初始化const T&类型的引用。

const T类型的常变量和const T&类型的引用则不能用来初始化T&类型的引用,除非进行强制类型转换

“const”关键字的用法

1)定义常量

define也可以定义常量,但是const定义的常量是有类型的

const int MAX_VAL = 23;
const double Pi = 3.14;
const char * SCHOOL_NAME= "Peking University";

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 = (int*)p1;//ok,强制类型转换

函数参数为常量指针时,可避免函数内部不小心改变参数指针所指地方的内容。

避免p所指向内容被修改。

strcpy函数中第一个类型是char *型,不可以用const char *赋值给char *

void MyPrintf(const char* p){
    strcpy(p,"this");//编译出错
    printf("%s",p);//ok
}

3)定义常引用

不能通过常引用修改其引用的变量

int n;
const int& r = n;
r = 5;//error
n = 4;//ok

动态内存分配

用new运算符实现动态内存分配(P109)

第一种用法,分配一个变量:

P = new T;

其中,T 是任意类型名,P 是类型为 T* 的指针。

动态分配出一片大小为sizeof(T) 字节的内存空间,并且将该内存空间的起始地址赋值给P。比如:

int* pn;
pn = new int;
*pn = 5;

第二种用法,分配一个数组:

P = new T[N];

T:任意类型名

P:类型为 T* 的指针

N:要分配的数组元素的个数,可以是整型表达式

动态分配出一片大小为 N*sizeof(T)字节的内存空间,并且将该内存空间的起始地址赋值给P。

动态分配数组示例:

int* pn;
int i = 5;
pn = new int[i * 20];
pn[0] = 20;
pn[100] = 30;//编译没问题。运行时导致数组越界

100个元素的数组的最大合法下标是99

new 运算符的返回值类型:

new T;

new T[n];

这两个表达式返回值的类型都是T *

int* p = new int;

用delete运算符释放动态分配的内存

用“new”动态分配的内存空间,一定要用“delete”运算符进行释放

delete 指针;//该指针必须指向new出来的空间
int* p = new int;
*p = 5;
delete p;
delete p;//error,一片空间不能被delete多次

用delete运算符释放动态分配的数组

用“delete"释放动态分配的数组,要加“[]"

delete []指针;//该指针必须指向new出来的数组
int* p = new int[20];
p[0] = 1;
delete[]p;

delete时如果没加 [],程序不会报错,但是不会把数组释放完全,造成内存的垃圾碎片

内联函数,函数重载,函数缺省参数

内联函数(P66)

当调用函数产生的开销太大时,可以使用内联函数。

函数调用是有时间开销的,如果函数本事只有几条语句,执行非常块,而且函数被反复执行很多次,相比之下调用函数所产生的这个开销就会显得比较大。

为了减少函数调用的开销,引入了内联函数机制。编译器处理对内联函数的调用语句时,是将整个函数的代码插入到调用语句初,而不会产生调用函数的语句。

在函数定义前面加”inline“关键字,即可定义内联函数

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;

函数重载(P67)

一个或多个函数,名字相同,然而参数个数参数类型不相同,这叫做函数的重载。

以下三个函数是重载关系:

int Max(double f1, double f2){ }
int Max(int n1, int n2){ }
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,二义性

重载函数:函数名相同,参数不同;

重复定义:函数名相同,参数相同,返回值不同;

函数的缺省参数(P61)

C++中,定义函数的时候,可以让最右边的连续若干个参数有缺省值,那么调用函数的时候,若相应位置不写参数,参数就是缺省值

void func(int x1, int x2 = 2, int x3 = 3){ }

func(10);//等效于 func(10,3,3)
func(10,8);//等效于 func(10,8,3)
func(10, ,8);//不行,只能最右边的连续若干个参数缺省

函数参数可缺省的目的:

提高程序的可扩充性。

如果每个写好的函数要添加新的参数,而原先那些调用该函数的语句,未必需要使用新增的参数,那么可以使用缺省参数,避免对原先那些函数调用语句进行修改。