从C走进C++(Week1&2)
函数指针
基本概念
- 程序运行期间,每个函数都会占用一段连续的内存空间
- 函数名就是该函数所占内存区域的起始地址(入口地址)
- 可以将函数的入口地址赋给指针变量,使该指针变量指向该函数,通过指针变量就可以调用这个函数
- 这种指向函数的指针变量被称为“函数指针”
定义形式
- 类型名(* 指针变量名)(参数类型1,参数类型2,......)
int (*pf)(int , char);
//pf为一个函数指针,它所指向的函数的返回值是int,2个参数一个是int类型一个是char类型
使用方法
- 可以用一个原型匹配的函数的名字给一个函数指针赋值
- 通过函数指针调用他所指向函数
- 函数指针名 (实参表)
#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;
}
//pf指针指向PrintMin
qsort库函数
- 对数组排序,需要知道
- 数组起始地址
- 数组元素个数
- 每个元素的大小(从而得出每个元素的地址)
- 元素谁前谁后的规则
void qsort(void *base, int nelem, unsigned int width, int(* pfCompare)(const void*,const void*));
//base:待排序数组的起始地址
//nelem:待排序数组的元素个数
//width:待排序数组的每个元素的大小(以字节为单位)
//pfCompare:比较函数的地址
//pfCompare:函数指针,它指向一个“比较函数”,该比较函数的形式如下
//int 函数名 (const void * elem1,const void * elem2);
//比较函数是程序员自己编写的
- 排序就是一个不断比较并交换位置的过程
- qsort函数在执行期间,会通过pfCompare指针调用“比较函数”,调用时将要比较的两个元素的地址传给“比较函数”,然后根据“比较函数”返回值判断哪个应该排在前面
- 比较函数编写规则
- 如果*elem1应该在前,函数返回负整数
- 如果*elem2应该在前,函数返回正整数
- 如果无所谓前后,函数返回0
- 实例
- 功能:调用qsort库函数,将一个unsigned int数组按照个位数从小到大进行排序
#include <stdio.h>
#include <stdlib.h>
int MyCompare(const void * elem1,const void * elem2)
{
unsigned int * p1, * p2;
p1 = (unsigned int *) elem1; //"*elem1" 非法,编译器不知道void指针指向的元素有多少个字节
p2 = (unsigned int *) elem2; //"*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;
}
命令行参数
以命令行方式运行程序
- notepad sample.txt
- notepad程序如何得知,用户在以命令行方式运行它的时候,后面跟着什么参数?
命令行参数
- 用户在CMD窗口输入可执行文件名的方式启动程序时,跟在可执行文件名后面的那些字符串,称为“命令行参数”。
- 命令行参数可以有多个,用空格分隔
- 举例
- copy file1.txt file2.txt
- "copy","file1.txt","file2.txt"就是命令行参数
- 如何获得命令行参数
- argc (argument counter):代表启动程序时,命令行参数的个数。C/C++语言规定,可执行程序程序本身的文件名,也算一个命令行参数,因此,argc的值至少是1
- argv (argument vector):指针数组,其中的每个元素都是一个char* 类型的指针,该指针指向一个字符串,这个字符串里就存放着命令行参数
- 提示: argument是实参,parameter是形参
- 由于命令行参数之间用空格分隔,如果其本身就含有空格,则可用双引号括起来
int main(int argc, char * argv[])
{
...
}
位运算
基本概念
- 位运算:
- 用于对整数类型(int, char, long...)变量中的某一位(bit)或者若干位进行操作。比如:
- 判断某一位是否为1
- 只改变其中某一位,而保持其他位都不变
- C/C++语言提供了六种位运算符来进行位运算操作:
- & 按位与(双目)
- |按位或(双目)
- ^ 按位异或 (双目)
- ~ 按位非(取反)(单目)
- <<左移(双目)
- >>右移(双目)
- 用于对整数类型(int, char, long...)变量中的某一位(bit)或者若干位进行操作。比如:
按位与“&”
-
将参与运算的两数,各对应的二进制位进行与操作,只有对应的两个二进制位均为1时,结果对应的二进制位才为1,否则为0
-
通常用来将某变量中的某些位清0切同时保留其他位不变,也可用来获取某变量中的某一位
按位或“|”
- 将参与运算的两操作数各对应的二进制位进行或操作,只有对应的两个二进制位都为0时,结果的对应二进制位才是0,否则为1
- 通常用来将某变量中的某些位置1且保留其他位不变
按位异或“^”
- 将参与运算的两操作数各对应的二进制位进行异或操作,只有对应的两个二进制位不相同时,结果的对应二进制位才是1,否则为0
- 通常用来将变量中的某些位取反且保留其他位不变
- 特点:如果有ab=c➡️cb=a 、c^a=b
- 能实现不通过临时变量就交换两个变量的值
int a = 5, b = 7;
a = a ^ b;
b = b ^ a;
a = a ^ b;
按位非“~”
- 将操作书中的二进制位0变成1,1变成0
左移运算符“<<”
- a << b
- 将a的各二进制位全部左移b位后得到的值。左移时,高位丢弃,低位补0,a本身的值不会被更改
- 左移1位就等于*2,左移n位就等于* $2^n$
右移运算符“>>”
- a >> b
- 将a的各二进制位全部右移b位后得到的值。右移时,移出右边的位会被丢弃,a本身的值不会被更改
- 对于有符号数,右移时,符号位将一起移动
- 大多C/C++编译器规定,如果原符号位位1,右移时高位补1,否则补0。
- 左移1位就等于/2,左移n位就等于/ $2^n$,并且将结果往小取整
引用
引用的概念
-
下面的写法定义了一个引用,并将其初始化为引用某个变量
类型名 & 引用名 = 某变量名
int n = 4;
int & r = n; //r引用了n,r的类型是int &
-
某个变量的引用,等价于这个变量,相当于该变量的一个别名
-
定义引用时一定要将其初始化成引用某个变量
-
初始化后,他就一直引用该变量,不会再引用别的变量了
-
引用只能引用变量,不能引用常量和表达式
引用的应用
- C语言中,如何编写交换两个整形变量值的函数?
void swap(int a, int b)
{
int tmp;
tmp = a; a = b; b = tmp;
}
int n1,n2;
swap(n1,n2); //n1n2的值不会被交换,形参的改变无法影响实参
void swap(int *a,int *b)
{
int tmp;
tmp = *a; *a = *b; *b = tmp;
}
int n1,n2;
swap(&n1,&n2); //n1n2的值被交换,但是多了很多符号,比较麻烦
void swap(int &a, int &b)
{
int tmp;
tmp = a; a = b; b = tmp;
}
int n1,n2;
swap(n1,n2); //n1n2的值被交换,由于a和b是n1和n2的引用,因而可以直接修改
- 引用作为函数的返回值
int n = 4;
int & SetValue() {return n;}
int main()
{
SetValue() = 40; //函数的返回值是引用,就可以把函数写在等号左边,可以直接赋值
cout<<n;//输出:40
return 0;
}
常引用
- 定义引用时,前面加const关键字,即为“常引用”
int n;
const int & r = n;
//r的类型是const int &
- 特点:不能通过常引用去修改其引用的内容
常引用和非常引用的转换
- const T & 和 T &是不同的类型(T为int,char等类型)
- T & 类型的引用或T类型的变量可以用来初始化const T & 类型的引用
- const T 类型的常变量和const T & 类型的引用则不能用来初始化 T & 类型的引用,除非进行强制类型转换
const关键字的用法
- 定义常量
const int MAX_VAL = 23;
const double Pi = 3.14;
- 定义常量指针
- 不可通过常量指针修改其指向的内容
- 不能把常量指针赋值给非常量指针,反过来可以
- 函数参数为常量指针时,可避免函数内部不小心改变参数指针所指地方的内容
int n,m;
const int *p = &n;
*p = 5; //编译错误
n = 4; //正确
p = &m; //正确,可以改变常量指针指向的对象
const int * p1; int * p2;
p1 = p2;//正确
p2 = p1;//错误
p2 = (int *)p1;//正确,通过强制类型转换
void MyPrintf(const char *p)
{
strcpy(p,"this");//编译错误
printf("%s",p);//正确
}
- 定义常引用
- 不能通过常引用修改其引用的变量
int n;
const int & r = n;
r = 5;//编译错误
n = 4;//正确
动态内存分配
用new运算符实现动态内存分配
- 第一种用法,分配一个变量
- 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;//虽然编译正确,但运行时会出现数组越界
- 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; //导致异常,一片空间不能够被delete多次
用delete运算符释放动态分配的数组
- 用“delete”释放动态分配的数组,要加“[]”
- delete [] 指针; //该指针必须指向new出来的数组
int *p = new int[20];
p[0] = 1;
delete [] p;
内联函数,函数重载和函数缺省参数
内联函数
-
函数调用存在时间开销。如果函数本身只有几条语句且执行非常快,而且函数被反复执行多次,相比其运行时间,调用函数所产生的时间开销就会很大。
-
为了减少该开销,引入了内联函数机制。编译器处理对内联函数的调用语句时,是将整个函数的代码插入到调用语句处,而不会产生调用函数的语句。
-
在函数定义前面加“inline”关键字,即可定义内联函数
-
缺点是可执行程序的体积会增大
函数重载
-
一个或多个函数,名字相同,然而参数个数或参数类型不相同,这叫做函数重载
-
以下三个函数是重载关系:
int Max(double f1,double f2){ } int Max(int n1,int n2){ } int Max(int n1,int n2,int n3){ }
-
函数重载简化函数命名
-
编译器根据调用语句中的实参的个数和类型判断应该调用哪个函数
-
函数缺省参数
- 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);//编译错误,只能最右边的连续若干个参数缺省
- 函数参数可缺省的目的在于提高程序的可扩充性
- 如果某个写好的函数要添加新的参数,而原先那些调用该函数的语句,未必需要使用新增的参数,那么为了避免对原先那些函数调用语句的修改,就可以使用缺省参数
面向对象程序设计方法
结构化程序设计
- 复杂的大问题➡️层层分解/模块化➡️若干子问题
- 自顶向下,逐步求精
- 程序 = 数据结构(变量)+算法(函数)
- 在结构化程序设计中,数据结构和算法没有直接关系
- 遇到的问题
- 理解难
- 修改难
- 查错难
- 重用难
面向对象的程序设计
- 软件设计的目标:更快,更正确,更经济
- 面向对象的程序设计 = 类 + 类 + …… + 类
- 设计程序的过程➡️设计类的过程
- 对一类事物进行抽象,提炼出共同属性(数据结构)和行为(函数),将数据结构和算法封装(捆绑)在一起,变成类。
面向对象语言的发展历程
-
第一个面向对象语言:Simula
- 1967年发布Simula 67
- 提出了类(class)和子类(subclass)的概念
-
第二个面向对象语言:Smalltalk
-
1983年 C++
-
1995年 JAVA
-
2003年 C#
C++标准的发展
- 1989年 C++2.0
- 1994年 ANSI C++
- 1998年 C++98
- 加入STL(Standard Template Library)-泛型设计
- 2003年 C++03
- 2011年 C++11
- 2014年 C++14
- 2017年 C++17
- 2020年 C++20
从客观事物抽象出类
- 写一个程序,输入矩形的宽和高,输出面积和周长
- 矩形的属性——宽和高两个变量
- 矩形的操作——设置宽和高,计算面积计算周长
- 类的成员=成员变量+成员函数
- 类就是一个带函数的结构体
- 类定义的变量➡️类的实例➡️对象
class CRectangle{
public:
int w,h;
void Init(int w_, int h_)
{
w = w_; h = h_;
}
int Area()
{
return w*h;
}
int Perimeter()
{
return 2*(w+h);
}
};
int main()
{
int w,h;
CRectangle r; //r是一个对象
cin>>w>>h;
r.Init(w,h);
cout<<r.Area()<<endl<<r.Perimeter();
return 0;
}
-
对象的内存分配
- 对象的内存空间
- 对象的大小 = 所有成员变量的大小之和
- e.g. CRectangle类的对象,sizeof(CREctangle) = 8
- 每个对象各有自己的存储空间
- 一个对象的某个成员变量被改变,不会影响到其他的对象
- 对象的内存空间
-
对象间的运算
- 对象之间可以用‘=’进行赋值
- 不能用== != > < >= <=进行比较
- 除非这些运算符经过了“重载“
-
访问类的成员变量和成员函数
- 用法1:对象名.成员名
- 用法2:指针->成员名
- 用法3:引用名.成员名
-
类的成员函数的另一种写法
- 成员函数体和类的定义分开写,在类内只声明,在类外详细定义(要加::)
类成员的可访问范围
- 关键字——类成员可被访问的范围
- private:指定私有成员,只能在成员函数内被访问
- public:指定公有成员,可以在任何地方被访问
- protected:指定保护成员
- 三种关键字出现的次数和先后次序都没有限制
- 如果缺省,就默认为私有成员
- 对象成员的访问权限
- 类的成员函数内部,可以访问:
- 当前对象的全部属性和函数
- 同类其他对象的全部属性和函数
- 类的成员函数以外的地方
- 只能访问该类对象的公有成员
- 类的成员函数内部,可以访问:
- 设置私有成员的目的
- 强制对成员变量的访问一定要通过成员函数进行
- 避免程序出错,并且易于对程序进行修改
- 设置私有成员的机制——隐藏
编程作业
Quiz1 简单的学生信息处理程序实现
#include<iostream>
#include<stdio.h>
#include<cstring>
#include<string>
#include<string.h>
//虽然我本地编译用最上面那个头文件就过了,但上传上去好像必须要加上后面这四个才能通过……行吧
using namespace std;
class student{
private:
char name[10];
int age;
char number[10];
unsigned int gradeAverage;
public:
void gradeCalculate(unsigned int a,unsigned int b,unsigned int c,unsigned int d)
{
gradeAverage = (a+b+c+d)/4;
}
student(const char* name_, int age_, const char* number_)
{
strcpy(name,name_);
age = age_;
strcpy(number,number_);
}
void getAll()
{
cout << name <<',' << age << ',' << number << ',' << gradeAverage << endl;
}
};
int main()
{
char name[10] = {' '},number[10] = {' '};
int age;
unsigned int grade1,grade2,grade3,grade4;
//此处的cin.get()用于把逗号吞掉
cin.getline(name,10,',');
cin >> age;
cin.get();
cin.getline(number,10,',');
cin >> grade1;
cin.get();
cin>> grade2;
cin.get();
cin >> grade3;
cin.get();
cin >> grade4;
student* a = new student(name,age,number);
a->gradeCalculate(grade1,grade2,grade3,grade4);
a->getAll();
delete(a);
return 0;
}