本专栏主要基于北大郭炜老师的程序设计与算法系列课程进行整理,包括课程笔记和OJ作业。该系列课程有三部分: (一) C语言程序设计;(二) 算法基础;(三) C++面向对象程序设计
1. 函数
为什么需要函数
- 写了一段牛顿迭代法求平方根的代码,程序里面无数地方都要求平方根, 难道需要的地方都把这段代码拷贝一遍?
- 一个数十万行的程序,都写在 main里面? 数百个程序员如何合写一个 main?
- “函数” 可以将实现了某一功能,并需要反复使用的代码包装起来形成 一个功能模块(即写成一个“函数”),那么当程序中需要使用该项功能时,只需写一条语句,调用实现该功能的 “函数”即可。
- 不同的程序员可以分别写不同的函数(功能模块),拼起来形成一个大程序
函数的定义
返回值类型 函数名(参数1类型 参数1名称, 参数2类型 参数2名称…)
{
语句组(即“函数体”)
}
- 如果函数不需要返回值,则“返回值类型”可以写“void”
- 函数参数可以有0个也可以有多个
函数调用和return语句
- 调用函数
函数名(参数1,参数2,…) - 对函数的调用,也是一个表达式。函数调用表达式的值,由函数内部的 return语句决定。return语句语法如下:
return 返回值; - return语句的功能是结束函数的执行,并将“返回值”作为结果返回。“ 返回值”是常量、变量或复杂的表达式均可。如果函数返回值类型为 “void”,return语句就直接写:
return ; - return 语句作为函数的出口,可以在函数中多次出现。多个return语句的 “返回值”可以不同。在哪个return语句结束函数的执行,函数的返回值就 和哪个return语句里面的“返回值”相等。
函数使用实例
- Max函数
#include <iostream>
using namespace std;
int Max(int x,int y) //求两个整型变量中的较大值 这里的x和y是形参
{
if( x > y )
return x;
return y;
}
int main() {
int n = Max(4,6); //调用时传入的参数为实参 **形参实参类型需兼容!**
cout << n << "," << Max(20,n) << endl;
return 0;
}
- 判断是否是素数的函数
#include <iostream>
using namespace std;
bool IsPrime(unsigned int n) {
if( n <= 1 )
return false;
for( int i = 2;i < n; ++i ) //看看有没有n的因子 其实可以只遍历到n的平方根 i<=int(sqrt(n))
if( n % i == 0 )
return false;
return true;
}
int main() {
cout << IsPrime(2) << "," << IsPrime(4) << "," << IsPrime(5);
return 0;
}
- 返回值为void的函数
void DrawCircle(double x,double y,double r)
{
//下面的代码在屏幕上以(x,y)点为圆心,r为半径画圆 .........
return;
}
调用:
DrawCircle(0,0,1);
- 已知三角形三个顶点位置, 求边长
给定平面上不共线的三个点,其坐标都是整数,编写程序,求它们构成的三角形的三条边的长度。输入是6个整数: x1,y1,x2,y2,x3,y3代表三个点的坐标 ,以任意顺序输出三条边的长度均可。
#include <iostream>
using namespace std;
#define EPS 0.001 //用以控制计算精度
double Sqrt(double a)
{ //用牛顿迭代法求a的平方根 (前面讲过,其实也可以直接用cmath库中的sqrt函数)
double x = a/2,lastX = x + 1 + EPS; //确保能够进行至少一次迭代
while( x - lastX > EPS || lastX - x > EPS) {
//只要精度没有达到要求,就继续迭代
lastX = x;
x = (x + a/x)/2;
}
return x;
}
double Distance(double x1,double y1,double x2,double y2)
{ //求两点(x1,y1),(x2,y2) 的距离
return Sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2));
}
int main()
{
int x1,y1,x2,y2,x3,y3;
cin >> x1 >> y1 >> x2 >> y2 >> x3 >> y3; cout << Distance(x1,y1,x2,y2) << endl; //输出(x1,y1)到(x2,y2)距离
cout << Distance(x1,y1,x3,y3) << endl; cout << Distance(x3,y3,x2,y2) << endl; return 0;
}
函数的声明
- 一般来说函数的定义必须出现在函数调用语句之前, 否则调用语句编译出错
- 如果函数A内部调用了B,B内部调用了A,哪个写前面?
- 此时可以使用函数的声明,把函数声明在调用语句之前即可,只要声明不一定需要定义。
返回值类型 函数名(参数1类型 参数1名称, 参数2类型 参数2名称…);
例如:
int Max(int a,int b);
double Sqrt(double);
double Distance(double,double,double,double);
参数名称可以省略,但是参数类型不可省略。函数声明也称为“函数的原型”。
void FunctionB(); //声明
void FunctionA() {
......
FunctionB();
......
return;
}
void FunctionB() {
.......
FunctionA();
.......
return;
}
main()函数
C/C++程序从main函数开始执行,执行到main中的return则结束。main()可以没有参数,实际上他有两个参数,在这里他的返回值类型为int,返回0就说明程序正常结束。
#include <iostream>
using namespace std;
int main()
{
cout << "Hello,world" << endl;
return 0;
}
函数参数的传递
- 函数的形参是实参的一个拷贝,且形参的改变不会影响到实参 (除非形参类型是数组、引用 — 引用之后会学习)
#include <iostream>
using namespace std;
void Swap(int a,int b) {
int tmp;
//以下三行将a,b值互换
tmp = a ;
a = b;
b = tmp;
cout << "In Swap: a=" << a << " b=" << b << endl;
}
int main()
{
int a = 4, b = 5;
Swap(a,b);
cout << "After swaping: a=" << a << " b=" << b;
return 0;
}
一维数组作为函数的参数
-
一维数组作为形参时的写法如下:
类型名 数组名[ ]
不用写出数组的元素个数。例如:
void PrintArray( int a[ ]) { } -
数组作为函数参数时,是传引用的,即形参数组改变了,实参数组也会改变。
-
编写一个求整型数组最大值的函数
#include <iostream>
using namespace std;
int a1[4] = {4,15,6,9};
int a2[] = {3,18,56,40,78}; //可以不指定大小
int FindMax( int a[] ,int length) { //length是数组长度
int mx = a[0]; //初始化最大值为第一个元素
for(int i = 1;i < length; ++i)
if( mx < a[i])
mx = a[i];
return mx;
}
int main()
{
cout << FindMax(a1,sizeof(a1)/sizeof(int)) << endl; //数组总体占的字节数除以数组元素类型占的字节数 得到数组长度
cout << FindMax(a2,sizeof(a2)/sizeof(int)) << endl;
return 0;
}
- 编写一个把int数组所有元素置0的函数
#include <iostream>
using namespace std;
int a1[4] = {4,15,6,9};
void SetToZero(int a[] ,int length) {
for(int i = 0;i < length; ++i)
a[i] = 0;
}
int main() {
SetToZero(a1,4);
for(int i = 0;i < 4; ++i)
cout << a1[i] << "," ; //0,0,0,0 形参变了 实参也跟着变了
return 0;
}
二维数组作为函数的参数
- 二维数组作为形参时,必须写明数组有多少列,不用写明有多少行:
void PrintArray( int a[][5])
{
cout << a[4][3];
}
- 必须要写明列数,编译器才能根据下标算出元素的地址。
a[i][ j]的地址:
数组首地址+i×N×sizeof(a[0][0])+j×sizeof(a[0][0])(N是数组列数,即每一行元素的个数)
形参数组的首地址就是实参数组的首地址
2. 递归初步
- 一个函数,自己调用自己,就是递归
- 递归定义:如果定义一个名词,其中出现了这个名词,即自己解释自己。
- 递归求阶乘(之前用循环做过)
int Factorial(int n)
//函数返回n的阶乘
{
if( n < 2)
return 1; //终止条件
else
return n * Factorial(n-1); //n! = n*(n-1)!
} // cout << Factorial(5); => 120
- 递归函数需要有终止条件,否则就会无穷递归导致程序无法终止甚至崩溃.
- 递归过程
int F (int n) //函数返回n的阶乘
{
if( n < 2)
return 1; //终止条件
else
return n * F (n-1);
}
- 求斐波那契数列第 n 项 (之前用循环做过)
int Fib(int n) {
if( n == 1 || n == 2)
return 1;
else
return Fib(n-1)+Fib(n-2);
}
以上两个例子是最简单的递归程序,递归很重要,可以解决很多复杂的问题,之后还会详细学习。
3. 库函数和头文件
- 难道真的求平方根都要自己写一个? No!!
#include <iostream>
#include <cmath> // 头文件<cmath>中包含许多数学库函数的声明 引入这个头文件 相当于该头文件中所有的函数声明 直接复制粘贴到这。
using namespace std;
int main()
{
double a;
cin >> a;
if(a<0) {
cout << "Illegal input" << endl;
return 0;
}
cout << sqrt(a); //调用标准库函数sqrt求平方根 sqrt在cmath头文件中声明
return 0;
}
库函数:C/C++标准规定的,编译器自带的函数
头文件:C++编译器提供许多“头文件”,如iostream cmath string等,里面包含库函数的声明,编译器的.lib文件中有具体库函数的定义或可执行指令。
头文件内部包含许多库函数的声明以及其他信息,如cin,cout的定义
#include
即可将头文件包含到程序中,此后即可使用头文件中声明的库函数及其他信息。
- 数学函数
数学库函数声明在cmath头文件中,主要有:
int abs(int x) //求整型数x的绝对值
double cos(double x) //求x(弧度)的余弦
double fabs(double x) //求浮点数x的绝对值
int ceil(double x) // 求不小于x的最小整数 浮点数向上取整
int floor(double x) //浮点数向下取整
int round(double x) //浮点数四舍五入 取整
double sin(double x) // 求x(弧度)的正弦
double sqrt(double x) // 求x的平方根
......
- 字符处理函数
这些库函数在ctype头文件中声明,主要有:
int isdigit(int c) //判断字符c是否是数字字符
int isalpha(int c) //判断字符c是否是字母
int isalnum(int c) //判断字符c是否是数字字符或字母
int islower(int c) //判断字符c是否是一个小写字母
int isupper(int c) //判断字符c是否是一个大写字母
int toupper(int c) //如果字符c是一个小写字母 则返回对应大写字母
int tolower (int c) //如果字符c是一个大写字母 则返回对应小写字母
4. 位运算
基本概念
用于对整数类型(int,char, long 等)变量中的某(二进制)一位(bit),或者若干位进行操作。比如:
1)判断某一位是否为1
2)只改变其中某一位,而保持其他位都不变。
C/C++语言提供了六种位运算符来进行位运算操作:
按位与’&’
将参与运算的两操作数各对应的二进制位进行与操作,只有对应的两个二进制位均为1时,结果的对应二进制位才为1,否则为0。
例如:表达式“21 & 18 ”的计算结果是16(即二进制数10000),因为:
21 用二进制表示就是:(int类型32位)
0000 0000 0000 0000 0000 0000 0001 0101
18 用二进制表示就是:
0000 0000 0000 0000 0000 0000 0001 0010
二者按位与所得结果是
0000 0000 0000 0000 0000 0000 0001 0000
通常用来将某变量中的某些位清0且同时保留其他位不变。也可以用来获取某变量中的某一位。
a & 1 = a //a=0/1
a & 0 = 0
例如,如果需要将int型变量n的低8位全置成0,而其余位不变,则可以执行:
n = n & 0xffffff00;
也可以写成:
n &= 0xffffff00;
如果n是short类型(16个2进制位)的,则只需执行:
n &= 0xff00
如何判断一个int型变量n的第7位(从右往左,从0开始数)是否是1 ?
只需看表达式 “n & 0x80”的值是否等于0x80即可。
0x80: 1000 0000
按位或 “|”
将参与运算的两操作数各对应的二进制位进行或操作,只有对应的两个二进位都为0时,结果的对应二进制位才是0,否则为1。
例如:表达式“21 | 18 ”的值是23,因为:
21: 0000 0000 0000 0000 0000 0000 0001 0101
18: 0000 0000 0000 0000 0000 0000 0001 0010
21|18: 0000 0000 0000 0000 0000 0000 0001 0111
按位或运算通常用来将某变量中的某些位置1且保留其他位不变。
a | 0 = a //a=0/1
a | 1 = 1
例如,如果需要将int型变量n的低8位全置成1,而其余位不变,则可以执行:
n |= 0xff;
0xff: 1111 1111
按位异或 “^”
将参与运算的两操作数各对应的二进制位进行异或操作, 即只有对应的两个二进位不相同时,结果的对应二进制 位才是1,否则为0。
例如:表达式“21 ^ 18 ”的值是7(即二进制数111)。
21: 0000 0000 0000 0000 0000 0000 0001 0101
18: 0000 0000 0000 0000 0000 0000 0001 0010
21^18: 0000 0000 0000 0000 0000 0000 0000 0111
按位异或运算通常用来将某变量中的某些位取反,且保留其他位不变。
a ^ 1 = 相反 //a=0/1
a ^ 0 = a
例如,如果需要将int型变量n的低8位取反,而其余 位不变,则可以执行:
n ^= 0xff;
0xff: 1111 1111
异或运算的特点是: 如果 a^b=c,则 c ^ b = a, c ^ a =b(穷举法可证)
此规律可以用来进行最简单的加密和解密。
另外异或运算还能实现不通过临时变量,就能交换两个变量的值:
int a = 5, b = 7;
a = a ^ b;
b = b ^ a;
a = a ^ b; //a=7 b=5
即实现a,b值交换。穷举法可证。
按位非 “~”
按位非运算符“~”是单目运算符。其功能是将操作数中的二进制位0变成1,1变成0。
例如,表达式“~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。a 的值不因运算而改变。
例如: 9 << 4
9的二进制形式:
因此,表达式“9<<4”的值,就是将上面的二进制数左移 4位,得:
0000 0000 0000 0000 0000 0000 1001 0000
即为十进制的144。
实际上,左移1位,就等于是乘以2,左移n位,就等于是 乘以 2 n 2^n 2n。而左移操作比乘法操作快得多。所以程序中如果用到将某数乘以 2 n 2^n 2n,可以直接用<<操作。
右移运算符 ‘>>’
表达式:
a >> b
的值是:将a各二进位全部右移b位后得到的值。右移时,移出最右边 的位就被丢弃。 a 的值不因运算而改变。
对于有符号数,如long,int,short,char类型变量,在右移时,符号位 (即最高位)将一起移动,并且大多数C/C++编译器规定,如果原符号位为1,则右移时高位就补充1,原符号位为0,则右移时高位就补充0。 无符号数,高位直接补0.
实际上,右移n位,就相当于左操作数除以 x n x^n xn,并且将结果往小里取整(可能除不尽)。
-25 >> 4 = -2 //-25/16 其实是-1.多的数 往小取整 得-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: 0000 0000 0000 0000 0000 0000 0000 1111
n1 >>= 2: 变成3
0000 0000 0000 0000 0000 0000 0000 0011 (有符号数 符号位一起右移,高位补符号位即0)
n2: 1111 1111 1111 0001
n2 >>= 3: 变成 fffe, 即-2
1111 1111 1111 1110 (有符号数 符号位一起右移,高位补符号位即1) (符号位是1 负数,绝对值是其他位按位取反 再加1 = -2)
上面程序中实际n2输出的是 fffffffe,这一因为,用%x输出十六进制形式时,其对应的是int,此时会把short类型转换为int类型。
n3: 1111 1111 1110 0000
n3 >>= 4: 变成 ffe
0000 1111 1111 1110 (无符号数 右移 高位直接补0)
用%x输出十六进制形式时,其对应的是int,此时会把 short类型转换为int类型。此时前面补16个二进制0,0可以省略 就是 ffe
c: 0000 1111
c >>= 3; 变成1
0000 0001 (有符号数 符号位一起右移,高位补符号位即0)
用%x输出十六进制形式时,其对应的是int,此时会把 char类型转换为int类型。此时前面补24个二进制0,0可以省略 就是 1.
位运算思考题
有两个int型的变量a和n(0 <= n <= 31), 要求写一个表达式,使该表达式的值和a的第n位相同。
该表达式能取出a的第n位。
解法1: (a>>n) & 1
有两个int型的变量a和n(0 <= n < 31), 要求写一个表达式,使该表达式的值和a的第n位相同。 如果n<31,可以有另一个答案
(a&(1<<n)) >>n
当n=31时,这个答案不对,如果符号位为1, a&(1<<n) 得到1000… 此时右移n位,前面全会补符号位1,就不是想要的答案了。
位运算十分重要,因为可以大大提高程序的运算速度,一些情况下,可以保证程序在OJ上提交时,不会超时。