程序设计与算法 | (11) 函数与位运算

本专栏主要基于北大郭炜老师的程序设计与算法系列课程进行整理,包括课程笔记和OJ作业。该系列课程有三部分: (一) C语言程序设计;(二) 算法基础;(三) 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上提交时,不会超时。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值