C / C++ 知识总结

文章目录


1. 程序设计和C语言 算法-程序的灵魂


  1. C语言是一门高级语言。 .c ——.obj —— .exe

  2. (一)1个简单的C语言程序由多个源程序组成(>=1)。
    1)预处理指令。
    2)全局声明。
    3)函数定义。
    (二)一个C语言程序是由一个或多个函数组成的,有且只有一个主函数main()

  3. float型是单精度的有效位数是7位( 整数部分 和小数部分一共7位)

  4. (a+1)=a[1] p[1]=(p+1)

  5. =(赋值运算符) ==(等于)

  6. 在电脑文件夹输入\10.51.2.100 打开共享文件夹

  7. set w(3)(中间间隔为3)

  8. c: 释放内存用free 申请空间用malloc与sizeof连用
    c++:释放内存用delete 申请空间用new

  9. %lf(双精度)%c(字符)%s(字符串)
    两个逗号不能挨着

  • 算术运算符>关系运算符>逻辑运算符>赋值运算符

    11.程序总是从main函数开始执行的。
    12.程序中对计算机的操作是由函数中的C语句完成的,c语言本身不提供输入输出语句。

    13. 算法的特性
    (1)有穷性
    (2)确定性
    (3)有零个或多个输入
    (4)有一个活多个输出
    (5)有效性

    14.算法的表示
    (1)自然语言
    (2)流程图(N-S)
    (3)伪代码
    (4)计算机语言

  1. 3种基本结构:顺序结构、选择结构、循环结构
    16. 结构化程序:
    (1)自顶向下
    (2)逐步细化
    (3)模块化设计
    (4)结构化编码·

17.实现两个整数的最大者。

 #include<stdio.h>

   int main()
    {
 	int a,b,c;
 	scanf("%d %d",&a,&b);
 	c=max(a,b);
 	printf("%d",c);
 	return 0;
    }
    int max(int x,int y)
    {
	int z;
	if(x>y)
	z=x;
	else
	z=y;
	return (z);
    } 
    

18.判断某一年是否为闰年。
分析:能被4整除,但不能被100整除的年份为闰年。
能被400整除的年份为闰年。

19.求多项式(1-1/2+1/3-1/4+…+1/99-1/100)的值。

 #include<stdio.h>
    int main()
    {
	int sign=1;
	double deno=2.0,sum=1.0,term;
	while(deno<=100)
	{
		sign=-sign;
		term=sign/deno;
		sum=sum+term;
		deno=deno+1;
	}
	printf("%f\n",sum);
	return 0;
    }               

2. 数据的表现形式


(一)常量
(1)整型常量。例如1000,-345。
(2)实型常量。

     1)十进制小数形式,有数加粗样式字和小数点组成。如:123.456
     2)指数形式 ,如12.34e3(代表12.34*10*10*10)规定e或E代表以10为底的指数。

(3)字符常量
1) 普通字符:(用单撇号括起来的一个字符)如:‘a’,‘Z’。

2)转义字符:如下图:
在这里插入图片描述
(4)字符串常量
用双撇号把若干个字符括起来,字符串常量是双撇号中的全部字符(但不包括双撇号本身)如"boy" “123”.

(5)符号常量
符号常量是在C语言中,可以用一个标识符来表示一个常量,这个标识符称之为符号常量。其特点是编译后写在代码区,不可寻址,不可更改,属于指令的一部分。

#define 标识符 常量
#define PI 3.1416经过以上指定后,本文件中从此行开始所有PI
都代表3.1416。
其中#define 也是一条预处理命令(预处理命令都以"#"开头),称为宏定义命令,其功能是把该标识符定义为其后的常量值。一经定义,以后在程序中所有出现该标识符的地方均代之以该常量值。习惯上符号常量的标识符用大写字母,变量标识符用小写字母,以示区别。

(二)变量

1)变量代表一个名字的、具有特定属性的一个储存单元。
2)变量必须先定义,后使用。
3)变量名实际上是以一个名字代表的一个储存地址。

C99允许常变量:
const定义:
形式为 :const type name = value;
例如:const int MONTHS = 12;

这样就可以在程序中使用MONTHS而不是12了。常量(如MONTHS)被初始化后,其值就被固定了,编译器将不允许再修改该常量的值。假如您这样做:
MONTHS = 18;
是不对的,就好像您将值4赋给值3一样,无法通过编译。
此外注意应在声明中对const进行初始化。下面的代码是不正确的:
const int toes;// toes的值此时是不确定的
toes=10;//这时进行赋值就太晚了
如果在声明常量时没有提供值,则该常量的值是不确定的,而且无法修改它

C语言数据类型
在这里插入图片描述
数据类型大小及表示的数据范围:
在这里插入图片描述

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

int main(){    
   printf("char占%d个字节\n", sizeof(char));
   printf("int占%d个字节\n", sizeof(int));
   printf("short占%d个字节\n", sizeof(short));
   printf("float占%d个字节\n", sizeof(float));
   printf("long占%d个字节\n", sizeof(long));
   printf("double占%d个字节\n", sizeof(double));
   unsigned char c = 128;
   printf("c = %d\n",c);
   system("pause"); 
   return 0;
 } 


3. 整型数据与字符型数据 运算符与表达式 数据的输入、输出(scanf、putchar、getchar、printf)


(一)整型数据

在C语言中,整型数据类型可分为:
char、short(short int)、int、long(long int)、long long(long long int)。

分析: 每一种整型数据类型可分为两种形式:无符号(unsigned)和有符号(signed)。
(1)基本类型(int型)

编译系统分配给int型数据2个字节或4个字节
在16位操作系统中:

   1)类型说明符:[signed] int,表示的数值范围:-32768 ~ 32768,存储大小:2字节
   2)类型说明符:unsigned int,表示的数值范围:0 ~ 65535,存储大小:2字节

在32位或64位操作系统中:

   1)类型说明符:[signed] int,表示的数值范围:-2,147,483,648 ~ 2,147,483,647,存储大小:4字节
   2)类型说明符:unsigned int,表示的数值范围:0 ~ 4,294,967,295,存储大小:4字节

(2)短整型(short int)

1) 类型说明符:[signed] short,表示的数值范围:-32768 ~ 32768,存储大小:2字节
2)类型说明符:unsigned short,表示的数值范围:0 ~ 65535,存储大小:2字节

(3)长整型(long int)

在32位操作系统中:

1)类型说明符:[signed] long,表示的数值范围:-2,147,483,648 ~ 2,147,483,647,存储大小:4字节
2)类型说明符:unsigned long,表示的数值范围:0 ~ 4,294,967,295,存储大小:4字节

在64位操作系统中:

1)类型说明符:[signed] long,表示的数值范围:-9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807,存储大小:8字节
2)类型说明符:unsigned long,表示的数值范围:0 ~ 18,446,744,073,709,551,615,存储大小:8字节

(4)双长整型(long long int)

1)类型说明符:[signed] long long,表示的数值范围:-9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807,存储大小:8字节
2)类型说明符:unsigned long long,表示的数值范围:0 ~ 18,446,744,073,709,551,615,存储大小:8字节

(二) 字符型数据

1.字符与字符代码

字符型数据就是字符。

字符型数据是用单引号括起来的一个字符。例如:

‘a’、‘b’、‘=’、‘+’、‘?’,都是合法字符型数据。

字符型数据有以下特点:

1.字符型数据只能用单引号括起来,不能用双引号或其它括号。
2.字符型数据只能是单个字符,不能是字符串。
3.字符可以是字符集中任意字符。但数字被定义为字符型之后就不能参与数值运算。如'5'和5 是不同的。'5'是字符型数据,不能参与运算。

2.字符变量

字符变量的类型说明符是 char
字符变量类型定义的格式和书写规则都与整型变量相同。例如:

char a,b;

字符变量在内存中的存储形式及使用方法

每个字符变量被分配一个字节的内存空间,因此只能存放一个字符。字符值是以ASCII码的形式存放在变量的内存单元之中的。

如x的十进制ASCII码是120,y的十进制ASCII码是121。对字符变量a、b赋予’x’和’y’值:

a=‘x’;

b=‘y’;

实际上是在a、b两个单元内存放120和121的二进制代码:

所以也可以把它们看成是整型量。C语言允许对整型变量赋以字符值,也允许对字符变量赋以整型值。在输出时,允许把字符变量按整型量输出,也允许把整型量按字符量输出。

3.浮点型数据

(1)float型

4个字节
以为8位表示指数部分,float型数据能得到6位有效数字

(2)double型

8个字节
可以得到15位有效数字

(3)long double型

字节    有效数字
8        15
16       19

运算符和表达式
在这里插入图片描述
1.运算符
在这里插入图片描述
举例:若有定义语句int a=12,则执行表达式a+=a-=a+a后a的值为多少?
若有定义语句int a=12,则执行表达式a+=a-=a+a后a的值为-24。

计算过程:
a+=a-=a+a,运算符和结合性,由运算符优先级,+优先级为4,-=和+=优先级为14,+结合方向从左至右,+=和-+从右至左。
所以:
先计算 (a+a),a=12,a+a=12+12=24,
再计算 a-=(a+a),即a=a-(a+a)=12-24=-12,
最后计算 a+=a, 即a=a+a=-12+(-12)=-24

2.自增、自减运算符

++ - -

i=3;
例如: j=++i;(i的值为4,j的值为4)
      j=i++;(i的值为4,j的值为3)

题目一:
1.给定一个大写字母,要求用小写字母输出

 #include<stdio.h>
int main()
{
 char c1,c2;
 c1='A';
 c2=c1+32;
 printf("%c\n",c2);
 printf("%d",c2);
 return 0;
 } 

3.复合赋值运算符

a+=3 等价于a=a+3;
x*=y+8等价于x=x*(y+8);
x%=3等价于x=x%3;

题目二:

求ax*x+bx+c=0的根。a,b,c,由键盘输入,设b*b-4ac>0.
#include<math.h>
int main()
{
 double a,b,c,disc,x1,x2,p,q;
 scanf("%lf%lf%lf",&a,&b,&c);
 disc=b*b-4*a*c;
 p=-b/(2.0*a);
 q=sqrt(disc)/(2.0*a);
 x1=p+q;
 x2=p-q;
 printf("x1=%7.2f\nx2=%7.2f\n",x1,x2);
 return 0;
}

4.用scanf函数输入数据

scanf(格式控制,地址表列)

使用过程一定要带取地址符&

例如 scanf("a=%f,b=%f,c=%f",&a,&b,&c)

5.字符数据的输入输出

(1)putchar 函数输出一个函数

putchar函数是单个字符输出函数。只输出一个字符。
putchar函数的基本格式为:putchar (c)

(1)当c为一个被单引号(英文状态下)引起来的字符时,输出该字符(注:该字符也可为转义字符);
(2)当c为一个介于0~127(包括0及127)之间的十进制整型数时,它会被视为对应字符的ASCII代码,输出该ASCII代码对应的字符;
(3)当c为一个事先用char定义好的字符型变量时,输出该变量所指向的字符。

(2) getchar函数输入一个字符
getchar()是c语言中的一个函数,可以用它来赋一个字符的值.

例如:char a;
  a=getchar();
当你在键盘上输入一个字符后按回车;(输入u)
那么字符变量a的值就是'u'了.
getchar函数只能接受一个字符

(3)printf输入
printf(“%d”, num); 中的%d相当于是一个占位符,它的作用是指明输出变量num的位置
拆开讲,%是告诉程序这里要打印一个变量,d是告诉程序打印的是十进制整数
所以%d就是告诉程序这里要打印一个十进制整数,你丫的先给我空着,从后面按顺序给我补上变量
同理,%f是告诉程序这里要打印一个浮点数


4.1 if 与switch 语句(含举例)


1 . if 语句

一般形式

if(表达式)语句1
[else 语句 2 ]

常用形式分为三种:

(1) i f(表达式) 语句1 (没有else 子句部分)

(2) if(表达式) 语句1
else
语句2
(3)if(表达式1) 语句1 (在else部分又嵌套了多层的if语句)
else if(表达式2) 语句2
else if(表达式3) 语句3
else if(表达式m) 语句m
else 语句m+1

举例1:兔子问题

假定一对大兔子每月能生一对小兔子,且每对新生的兔子经过一个月可以长成一对大兔子,具有繁殖能力;
如果刚开始有1对小兔子,不发生死亡,且每次均生下一雌一雄,问20个月后共有多少对兔子?
(提示:这是个斐波那契数列,从第三项开始,之后的每-项都等于前两项的和: 1,1,2,3,5,8,13…虽然不知道为什么第三个月才变成2,不过还是斐波那契说了算,1、1是前两项,输出斐波那契数列的前20项就行了。)

#include <stdio.h>
int main()
{
    int i,a,b,c,m;
    a=1;
    b=1;
    printf("请输入月份数: ");
    scanf("%d",&m);
    if(m==1||m==2)
    {
        printf("有一对兔子");
    }
    else if(m>2)
    {
        for(i=3;i<=m;i++)
        {
            c=a+b;
            a=b;
            b=c;
        }
        printf("%d 月的兔子数为:%d对\n",m,c);
    }
    return 0;
}

举例二水仙花数
输入一个数,判断是不是水仙花数.

#include<stdio.h>
int main()
{
	int n;
	printf("请输入一个三位数:");
	scanf("%d",&n);
	int i,j,k;
	if(n>=100&&n<=999)
	{
		i=n/100;     //获取百位数字 
		j=n/10%10;   //获取十位数字 
		k=n%10;      //获取个位数字 
		int x=i*i*i+j*j*j+k*k*k;
		if(x==n)
		{
			printf("%d是水仙花数",n);
		}
		else
		{
			printf("%d不是水仙花数",n);
		}
	} 
	else
	{
		printf("输入的数不符合要求-_-");
	}
	return 0;
}


# include <stdio.h>
int main()
{
    int i,j,k,n;
    printf("水仙花数:\n");
    for (n=100; n<1000; n++) 
    {
        i=n/100;
        j=(n-i*100)/10;
        k=n%10;
        if(i*i*i+j*j*j+k*k*k==n)
        {
            printf("%d ",n);
        }
    }
}

举例三:输入一个数值,判断该数值属于哪一个范围,输出对应的结果

#include<stdio.h>
int main()
{
	int grade;
	printf("请输入你的成绩:");
	scanf("%d",&grade);
	if(grade>=90&&grade<=100)
	{
		printf("A");
	}
	else if(grade>=70&&grade<90)
	{
		printf("B");
	}
	else if(grade>=60&&grade<70)
	{
		printf("C");
	}
	else if(grade>=0&&grade<60)
	{
		printf("D");
	}
	else
	{
		printf("输入的成绩有误!"); 
	}
	return 0;
}

举例四: 输入一个年份,判断该年份是不是闰年

#include <stdio.h>

int main()
{
	int year;
	printf("请输入年份: ");
	scanf("%d", &year);
	if( (year % 4 == 0 && year % 100 != 0) || year % 400 == 0){
		printf("%d 是闰年", year);
	}else{
		printf("%d 不是闰年", year);
	}
	return 0;
}

举例五:输入三个数 a,b,c,要求按由小到大的顺序输出

#include<stdio.h>
int main(){
	int a,b,c;
	int t;
	scanf("%d %d %d",&a,&b,&c);
	if(a>b)
	{
		t=a;
		a=b;
		b=t;
	}
		if(a>c)
	{
		t=a;
		a=c;
		c=t;
	}
	if(b>c){
		t=b;
		b=c;
		c=t;
		
	}

	printf("%d %d %d",a,b,c);
}

4.2 逻辑运算符与逻辑表达式


1.运算符的等级关系:

算术运算符 > 关系运算符 >赋值运算符

2.关系运算符

(1) < <= > 优先级相同(高)
(2) >= == != 优先级相同(低)

3.逻辑运算符与逻辑表达式

(1) && (与) ||(或) !(非)

逻辑运算符的优先级顺序为:
小括号() > 负号 > ! > 算术运算符 > 关系运算符 > && > ||

(2)逻辑表达式

例1:表达式!(3>5) ||(2<4) && (6<1)

先计算 !(3>5)、(2<4)、(6<1),结果为1,式子变为1 || 1 && 0,再计算1 && 0,式子变为1 || 0,最后的结果为1

例2:表达式3+2<5||6>3 等价于 ((3+2) < 5) || (6>3),结果为1

例3:表达式4>3 &&!-5>2 等价于 (4>3) && ((!(-5)) > 2),结果为0


4.3 关系运算符与三目运算符


三目运算符:

表达式1?表达式2 :表达式 3
max=(a>b)? a:b;

如果(a>b)为真,则条件表达式的值为a,否则条件表达式的值为 b;

举例:
输入3个数,然后输出最大的数值

#include <stdio.h>
int main()
{
	int n1,n2,n3;
	printf("请输入3个数: \n");
	scanf("%d %d %d", &n1, &n2, &n3);
	printf("%d", n1 > n2 ? (n1 > n3 ? n1 : n3) : (n2 > n3 ? n2 : n3));
	return 0;	
}

举例二:
输入3个数,然后输出最小的数值

#include<stdio.h>
int main()
{
	int a,b,c;
	printf("请输入三个数:");
	scanf("%d %d %d",&a,&b,&c);
	int min=(a<b?a:b)<c?(a<b?a:b):c;
	printf("%d",min);
	return 0;
}

5.选择结构的嵌套

if()
if() 语句1

else 语句2

else
if() 语句3
else 语句4

注意 if和else 的一 一配对

6.switch 语句

switch(表达式)
{
case 常量1:语句1
case 常量2:语句2

case 常量 n: 语句n
default: 语句n+1
}

意思是先计算表达式的值,再逐个和case 后的常量表达式比较,若不等则继续往下比较,若一直不等,则执行default后的语句;若等于某一个常量表达式,则从这个表达式后的语句开始执行,并执行后面所有case后的语句

举例:释放技能 Q、W、E、R

#include<stdio.h>
int main()
{
	char  a;
	scanf("%c",&a);
	switch(a)
	{
		case 'Q':printf("折翼之舞释放成功!\n");break;
		case 'W':printf("震魂怒吼释放成功!\n");break;
		case 'E':printf("勇往直前释放成功!\n");break;
		case 'R':printf("放逐之锋释放成功!\n");break;
		default:printf("error\n");
	}
	return 0;
 } 
 


5.1 循环结构(while语句、do while语句、for语句)


1. while(表达式)语句

while语句的一般形式如下:

简单地记为:只要当循环条件表达式为真(即给定的条件成立),就执行循环体语句。
while循环的特点是:先判断表达式,后执行循环体语句

举例 求1+2+3+4+…+100的值

  #include<stdio.h>
       int main()
       {
        int i=1;
        int sum=0;
       while(i<=100)
        {
       sum=sum+i;
        i++;
       }
       printf("sum=%d\n",sum);
        return 0;
       } 
标注:凡用while循环完成的,用for循环都能实现

举例二:求最小公倍数和最大公因数

#include <stdio.h>
int main()
{
    int a,b,c,m,t;
    printf("请输入两个数:\n");
    scanf("%d%d",&a,&b);
    if(a<b)
    {
        t=a;
        a=b;
        b=t;
    }
    m=a*b;
   c=a%b;
    while(c!=0)
    {
        a=b;
        b=c;
        c=a%b;
    }
    printf("最大公约数是:\n%d\n",b);
    printf("最小公倍数是:\n%d\n",m/b);
}

举例三:计算 π=(1-1/3+1/5-1/7+1/9-1/11…)*4 的值。

# include <stdio.h>
int main( )
{
   int i = 1;
   int j = 1;
   double sum = 0;  //结果肯定是小数, 所以要定义成double或float型
    while (1.0/i > 1e-6)  /*当1/i小于10的-6次方时停止循环。这个循环条件是自己定的, 定得越小最后的结果就越精确。注意1一定要写成小数的形式即1.0*/
    {
        sum += (1.0 / i) * j;
        i+=2;
        j = -j;  //实现正负交替
    }
    sum *=4; //  a+=2 --> a=a+2;  sum*=4 --> sum=sum*4;
    printf("sum = %lf\n", sum);  //double是%lf, 取6位小数是%.6
    return 0;
}

2. do…while 语句

do…while语句的特点是:

先无条件地执行循环体,然后判断循环体是否成立

do…while语句的一般形式为

do
语句
while(表达式);

提示: do…while,while后面有;

举例: 求1+2+3+4+…+100的值

 #include<stdio.h>
     int main()
    {
     int i=1;
     int sum=0;
     do
     {
     sum=sum+i;
     i++;
     }
     while(i<=100);
    printf("sum=%d\n",sum);
    return 0;
    }

3.for语句

  1. 语句最简形式为:

for( 表达式1;表达式2 ;表达式3 )

表达式1:循环变量赋初值,只执行一次。可以为零个、一个或多个变量设置初值。

表达式2:循环条件,用来判定是否继续循环。在每次执行循环体前先执行此表达式,决定是否继续执行循环。

表达式3:循环变量增值,例如使循环变量增值,它是在执行完循环体后才进行的。

2… 一般形式为:

for(循环变量赋初值;循环条件;循环变量增值)
{
中间循环体;
}

举例1:打出字母Y

  #include<iostream>
      using namespace std;
      int main()
     {
     int n;
     cin>>n;
    int m=(n+1)/2;
 
    for(int i=m;i>=1;i--)
    {
    for(int k=1;k<=m-i;k++)
    cout<<" ";
  
    for(int j=1;j<=2*i-1;j++)
    {
    if(j==1||j==2*i-1)
    {
    cout<<"*";
    }
    else
    {
    cout<<" ";
    }
    }
    cout<<endl;
    }
    for(int i=0;i<m;i++)
    {
    for(int k=1;k<=m-1;k++)
    {
    cout<<" ";
      }
    cout<<"*"<<endl;
    }
    return 0;
    }

举例2:逆序输出

#include <stdio.h>
#include <math.h>

int main() {
	int a,c;
	int sum = 0;
	int p, cha;
	scanf("%d", &a);
	c = a;  //把数字传递给变量c(由于需要正序输出和逆序输出)

	// (1)统计数字个数
	while(c!= 0) {
		c/= 10;
		sum++;
	}
	printf("当前输入的位数:%d\n", sum);
	
	//(2)数字正序输出
	c = a;
	for(int i = sum-1; i >= 0; i--) {
		p= (int)pow(10, i);//pow(x,y)为x的y次方
		cha = c/p;
		printf("%d", cha);//第一次循环,先输出第一位
		c=c-(p*cha);//      通过这一步,进入下一次循环。
	}
	printf("\n");
	
	//(3)取余数倒序输出
	while(a!= 0) {
		printf("%d", a%10);
		a /= 10;
	}
	return 0;
}
#include<stdio.h>
#include<string.h>

int main(){
	int i;
	char b[100];
    scanf("%s",&b);
	int len=strlen(b);		
	printf("长度为:%d\n",len);
	for(i=0;i<strlen(b);i++)
	{
		printf("%c ",b[i]);
	}
	printf("\n");  //换行 
	for(i=strlen(b)-1;i>=0;i--)
	{
		printf("%c",b[i]);
	}
	
}

举例3:打出九九乘法表

#include <stdio.h>

int main()
{
	int i,j;
	for(i = 1; i <= 9; i++){
		for(j = 1; j <= i; j++){
			printf("%d * %d = %d\t", j, i, i * j);
		}
		printf("\n");
	}
	return 0;	
} 

举例4:求出100以内的素数

#include <stdio.h>

int main()
{
	int i,j;
	for(i = 2; i <= 100; i++){
		for(j = 2; j < i; j++){
			// 判断i是否有其他余数,如果有就break掉
			if(i % j == 0){
				break;
			}
		}
		// 循环结束,如果i==j,则这个数为素数
		if(j == i){
			printf("%d\n", i);
		}
	}
	return 0;
}

举例五:打出等腰三角形

#include<stdio.h> 
int main()
{
	int i,j,n;
	scanf("%d", &n);
	for(i = 1; i <= n; i++){
		// 打印空格 
		for(j = 1; j <= n - i; j++){
			printf(" ");
		}
		// 打印* *的规律为 2*n-1
		for(j = 1; j <= 2 * i - 1; j++){
			printf("*");
		}
		printf("\n");
	}
	return 0;	
} 

举例六:
试计算在区间 1 到 n 的所有整数中,数字 x (0≤x≤9)共出现了多少次?例如,在1到11中,即在 1,2,3,4,5,6,7,8,9,10,11 中,数字 1 出现了 4 次。
输入样例:11 1 输出样例:4

#include<stdio.h>
    int main()
    {
    	int n,x,i,k=0;
    	scanf("%d %d",&n,&x);
    	for(i=1;i<=n;i++){
    		int j=i;
    	         while(j!=0){
    		if(j%10==x) k++;
    		j/=10;
    	    }
    	}
    	printf("%d ",k);
    }

举例七: 使用for与应用输出一个❌

#include<stdio.h>
int main() {
	int i, j;
	for (i = 0; i < 10; i++) {
		for (j = 0; j < 9; j++)
			if (j == i || j == 8 - i)
				printf("*");
			else printf(" ");
		printf("\n");
	}
	return 0;
}

举例八:打印倒三角

#include <stdio.h>

int main()

{
	int i,j,n;

	scanf("%d",&n);        //输入n的值

	for(i=0; i<n; i++)      //共n行

	{

		for(j=0; j<i; j++)      //前面的空格

			printf(" ");

		for(j=0; j<2*(n-i)-1; j++)      //输出一行上的“*”

			printf("*");

		printf("\n");                //一行结束,换行

	}
	return 0;

}

举例九:空心等边三角形

#include<stdio.h>
int main()
{
	printf("请输入行数:") ; 
	int n;    //三角形有多少行
	scanf("%d",&n);
	int i,j;
	for(i=1;i<=n;i++)
	{
		if(i!=n)    //if   i不等于4 
		{
			for(j=1;j<2*n-1;j++)    //遍历 1~9的每个位置 
			{
				if(j==n-i+1||j==n+i-1)	 printf("*");   
				else                     printf(" "); 	
			} 
			printf("\n");
		}
		else
		{
			for(j=1;j<=n;j++)
			{
				printf("* ");
			}
		}
		
	}
	return 0;
}

举例10:最大公约数和最小公倍数

#include<stdio.h>
int main()
{
	int m,n,k,j,g;
	printf("请输入两个数:");
	scanf("%d %d",&m,&n);
	if(m>n) k=n; 
	else k=m;
	for(int i=k;i>=1;i--)
	{
		if(m%i==0&&n%i==0){
			printf("最大公约数是%d",i);
			g=i;
			break;
		}
		else 
		continue;
	}
	j=(m*n)/g;
	printf("最小公倍数是%d",j);
}

举例11:累乘相加

#include<stdio.h>
int main()
{
	int n,t=0;
	printf("请输入阶乘累加数值:");
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
	    int sum=1; 
		for(int j=1;j<=i;j++)
		{
			sum*=j;
		}
		t+=sum; 
	}
	printf("求和为:%d",t);
}

实心菱形

#include<stdio.h>
int main(){
	int i,j,k,m;
	printf("请输入菱形边长:"); 
	scanf("%d",&m);
	
	for(i=1;i<=m;i++){
       for(k=0;k<m-i;k++){
          printf(" ");
       }
    printf("*");
    
    if(i==1){
      printf("\n");
      continue;
    }
   
    for(j=0;j<2*i-3;j++){
       printf(" ");
    }
       printf("*");
       printf("\n");
    }
    
    for(i=m-1;i>0;i--){
       for(k=0;k<m-i;k++){
       printf(" ");
    }
    printf("*");
    
    if(i==1){
    printf("\n");
    continue;
    }
    
    for(j=0;j<2*i-3;j++){
    printf(" ");
    }
    
    printf("*");
    printf("\n");
}
    return 0;
} 
#include<stdio.h>
int main() 
{
	int i,j,k,n;
	printf("请输入菱形的边长:"); 
	scanf("%d",&i);
	printf("\n\n\n\n"); 
	
	for(j=1;j<=i;j++)
	{
		for(k=j;k<=i-1;k++)
		{
			printf(" ");
		}
		for(n=1;n<=2*j-1;n++)
		{
			printf("*");
		}
		printf("\n"); 
	}
	
	for(j=1;j<=i;j++)
	{
		for(k=1;k<=j;k++)
		{
			printf(" ");
		}
		for(n=1;n<=(i-j)*2-1;n++)
		{
			printf("*");
		}
		printf("\n"); 
	}	
}

5.2 循环的嵌套和改变循环的状态


1.循环的嵌套

   (1)while()
            {
              while()   (内层循环)
              {...}
             }
             
  (2)do
    {
       do    (内层循环)
       {..}
       while()
       }
       while()
       
 (3)for(; ;)
     {
      for(; ;)    (内层循环)
      {...}
      }

(4)while(){
       ...
       do    (内层循环)
      {...}
      while();
        ...
      }

(5) for(; ;)
     {
       while()        (内层循环)
       {...}
         ...
       }

(6) do{
         ...
      for(; ;)     (内层循环)
         {..} 
         ...
    }  while();

2.改变循环的状态

  1. break语句(结束所有循环)
               break语句的一般形式:
                  break;    
  其作用是是流程跳到循环体之外,接着执行循环体下面的语句。
注意 :break语句只能用于循环语句和switch语句之中,而不能单独使用。
  1. continue语句(跳出本次循环)
    continue语句的一般形式:
    continue;

    其作用是结束本次循环,即跳过循环体中下面尚未执行过的语句,接着执行for语句中的表达式,然后进行下一次是否执行循环的判定。

3.break和continue语句的区别

continue只是结束本次循环,而break结束整个循环过程,不再判断执行循环的条件是否成立。


6.1 一维数组和二维数组


一维数组

1.定义一维数组的一般形式为
类型符 数组名【常量表达式】

int a [10](40个字节)
他表示定义了一个整型数组,数组名为a,此数组有10个整型元素。
注意下标是从0开始的,即a[0]~a[9]。

说明:
(1)数组名的命名规则和变量名相同,遵循标识符命名规则。
(2)常量表达式中可以包含常量和符号常量。如
int a[3+5]是合法的。
但不能包含变量,如int a[n]是不合法的。

2.一维数组的引用

(1)引用数组元素的表示形式为:
          数组名 [下标]
 (2)定义数组时用得“数组名[常量表达式]”和引用数组元素时的“数组名[下标]”形式相同,    但含义不同。
例如:
int a  10];   //这里的a[10]表达的是定义数组是指定数组包含十个元素
t=int a [6];//这里的a[6] 表达引用a数组中序号为6的元素

3.一维数组的初始化

(1)在定义数组是对全部数组元素赋予初值。例如:
int a[10]={0,1,2,3,4,5,6,7,8,9};
(2)可以只给数组中的一部分元素赋值。
int a [10]={0,1,2,3,4};

(3)如果想使一个数组中全部元素值为零,可以写成a[10]={0}

(4)在对全部元素赋初值时,由于数据的个数已经确定,因此可以不指定数组长度。例如 :int a[]={1,2,3,4,5};

二维数组

   (1) 二维数组定义的一般形式为:
  类型说明符 数组名[常量表达式] [常量表达式]
	例如: float a [3][4]      //  定义a为3*4(3行4列)的数组
		

   (2) 二维数组的 引用
	二维数组元素的表达形式为:
		数组名[下标][下标]
	数组元素可以出现在表达式中,也可以被赋值,例如:
	b[1][2]=a[2][3]/2
	注意数组大小范围

	int a[3] [4];
	不存在int a[3][4]=3;
	数组a可用的“行下标”的范围:0~2。
	“列下标”的范围为:0~3。

二维数组的初始化

(1)分行给二维数组赋初值。
int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};

(2)可以将所有数据写在一个花括号内。
int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};


 (3)可以对部分元素赋值。
  int a[3][4]={{1},{5},{9}};
更清晰的说明:
  1 0  0  0
  5 0  0  0
  9 0  0  0

(4)如果对全部元素都赋初值,则定义数组时对第一数组的长度可以不指定,但第二维的长度不能省。

int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12}等价于int a[][4]={1,2,3,4,5,6,7,8,9,10,11,12}

二维数组的应用举例:

#include<stdio.h>
int main()
{
 int a[3][3]={{1,2,3},{4,5,6},{7,8,9}};
 int i,j,k;
 for(i=0;i<3;i++)
 for(j=0;j<2;j++)
 {
  if(a[i][j]>a[i][j+1])
  {
   k=a[i][j];
   a[i][j]=a[i][j+1];
   a[i][j+1]=k;
  }
 }
 for(i=0;i<3;i++)
 for(j=0;j<3;j++)
 printf("%4d",a[i][j]);
}


6.2 字符数组 以及 函数puts gets strcat strcpy strncpy strcmp strlen strlwer struper的用法


字符数组

(1)字符数组中的一个元素存放一个字符。定义字符数组的方法与定义数值型数组的方法类似。
		char c[10];
	c[0]='I';  c[1]='  ',c[2]= ' a ' 


(2)字符数组的初始化
1.最容易理解的方式是用“初始化列表”,把各个字符依次赋给数组中各元素。
例如: char [10]={'i',' ','a','m',' ','h','a','p','p','y'};
		char []={'i',' ','a','m',' ','h','a','p','p','y'};


	2.用字符串常量使字符数组初始化。
	例如:
	char[]={"I am happy"};
	可以省略花括号,直接写成 char []="I am happy";

字符数组的结束标志

	‘\0’字符数组结束的标志,‘\0’为ASCII码为0的字符。

	字符数组并不要求它的最后一个字符为‘\0’,甚至可以不包含'\0'.、

	比如: char c[5]={'C','H','I','M','N'}

	是否需要加'\0',完全根据情况而定。

	比如 char c [6]={'C','H','I','M','N','\0'}

字符数组的输入输出

char c []={"china"}
scanf("%s",c);
printf("%s",c);

说明:
(1)输出的字符中不包括结束符’\0’。
(2)如果一个字符数组中包括一个以上’\0’,则遇到第一个’\0’时输出就结束。

使用字符串处理函数

1.puts 函数(输出字符串的函数)

	puts()函数用来向标准输出设备(屏幕)输出字符串并换行。

具体为:把字符串输出到标准输出设备,将'\0'转换为回车换行。
其一般形式为:
puts(s);其中s为字符串字符(字符串数组名或字符串指针)。

2.gets 函数(输入字符串的函数)

gets()函数用来从标准输入设备(键盘)读取字符串直到换行符结束,但换行符会被丢弃,然后在末尾添加'\0'字符。其调用格式为:
gets(s);	//其中s为字符串变量(字符串数组名或字符串指针)。
gets(s)函数与scanf("%s",s)相似,但不完全相同,使用scanf("%s",s) 函数输入字符串时存在一个问题,就是如果输入了空格会认为字符串结束,空格后的字符将作为下一个输入项处理,但gets()函数将接收输入的整个字符串直到遇到换行为止

也就是说:gets()函数读取到\n(我们输入的回车)于是停止读取,但是它不会把\n包含到字符串里面去。
然而,和它配合使用的puts函数,却在输出字符串的时候自动换行。

注意:用puts和gets函数只能输出或输入一个字符串,不能写成puts(str1,str2);

3.strcat函数(字符串连接函数)

其一般形式:
strcat(字符数组1,字符数组2)
作用是把两个字符串连接起来,把字符串2接到字符串1的后面,结果放在字符数组1中

注意:字符数组1必须足够大,以便容纳连接后的新字符串。连接后'\0'放在最后只有一个。

4. strcpy和strncpy函数——字符串复制函数

strcpy表示字符串复制函数,作用是将字符串2复制到字符数组1中去。
其一般形式为:

char str1[10], char str2 []="china"
strcpy(str1,str2);

注意:不能用赋值语句将一个字符串常量或字符数组直接给一个字符数组。
比如 :str1=“china”;str1=str2;

用赋值语句只能将一个字符赋给一个字符型变量或字符数组元素。

strncpy

可以用strncpy将字符串2中前面n个字符复制到字符数组1中去。例如:

strncpy(str1,str2,2);

作用是将str2中最前面两个字符复制到str1中,取代str1中原有的最前面的两个字符。但复制的字符个数n不应多于str1中原有的字符(不包括'\0')

5.strcmp函数——字符串比较函数

其一般形式为

strcmp(字符串1,字符串2)

字符串比较的规则是:将两个字符串自左向右逐个字符相比(按ASCII码值的大小比较,直到出现不同的字符或遇到'\0'结束。

如果全部字符相同,则两个字符相等。
(1) 如果字符串1=字符串2,则函数值为0;

(2) 如果字符串1>字符串2,则函数值为正值;

(3) 如果字符串1<字符串2,则函数值为负值; 提示:两个字符串比较,不能用if(str1>str2),只能用if(strcmp(str1,str2)>0)

6.strlen函数——测字符串长度的函数

其一般形式为:

strlen(字符数组)(测得是实际长度)

比如: char str1[10]="china";
	 printf("%d",strlen(str1)); //	结果为5

7.strlwer函数——转换为小写的函数

其一般形式为:

		strlwer(字符串)

8.struper函数——转化为大写的函数

其一般形式为:

		struper(字符串)

7.1 函数的定义 调用函数 实参和形参 函数的声明和函数原型 递归


1.函数就是功能,函数名字应反映其代表的功能。

2.一个C程序可由一个主函数和若干个其他函数构成。函数是可以调用的,但不能调用main函数。main函数是被操作系统调用的。

(一)定义函数的方法。

(1)定义无参函数的一般形式为:

	  类型名 函数名()

{

 函数体

}

或 

 类型名   函数名(void)

{

函数体

}

函数体应包括声明部分和语句部分。

(2)定义有参函数

一般形式为

类型名 函数名 (形式参数表列)

{

函数体

}

函数体包括声明部分和语句部分。

(3)定义空函数

类型名 函数名()

{ }

例如:

void dummy()

{ }

(二)调用函数

print-star();        //调用无参函数

c=max(a,b);//调用有参函数

一般形式为:

函数名 (实参列表)

如果是调用无参函数,则实参列表可以没有,但括号不能省略。

函数参数

函数调用作为另一个函数调用时的参数。例如:m=max(a,max(b,c))

调用函数并不一定要求包括分号(如 print_star();)

(三)实参和形参

在定义函数时函数名后面括号中的变量名称为“形式参数”。

在主调函数中调用一个函数时,函数后面括号中的参数称为“实际参数”,实际参数可以是	常量,变量,表达式。





(1)在调用函数的过程中,系统会把实参的值传递给被调用函数的形参。或者说,形参从实参得到一个值。

(2)实参与形参的类型应相同或赋值兼容。

(3)调用结束,形参单元被释放。注意:实参单元应保留并维持原值,没有改变。

(4)实参想形参的数据传递为值传递,单向传递,只能由实参传给形参。实参和形参在内存中占有不同的储存单元,实参无法得到形参的值。

(四) 对被调用函数的声明和函数原型。

(1)在函数声明中的形参名可以省写,而只写形参的类型类型。

float add(float x,float y)  可以写成 float add(float,float);


(2)1.函数类型 函数名(参数类型 1 参数名1,参数类型2 参数名2,... ,参数类型n,参数名 n);

     2.函数类型 函数名 (参数类型1,参数类型2,参数类型3,...,参数类型n)

(五)函数的递归调用

在调用一个函数的过程中又出现直接或间接地调用该函数本身,称为函数的递归调用。(c语言的特点之一)

例如:

   int f(int x)

    {

       int y,z;

            z=f(y);

        return  (2*z);

         }

汉诺塔问题。

#include<stdio.h>
int main()
{
    void hanoi(int n,char one,char two,char three);
    int m;
    printf("input the number of diskes:");
    scanf("%d",&m);
    printf("The step to move %d diskes:\n",m);
    hanoi(m,'A','B','C'); 
 } 
    void hanoi(int n,char one,char two,char three)
    {
        void move(char x,char y);
        if(n==1)
        move(one,three);
        else
        {
            hanoi(n-1,one,three,two);
            move(one,three);
            hanoi(n-1,two,one,three);
        }
    }
void move(char x,char y)
{
    printf("%c->%c\n",x,y);
} 

(六)数组作为函数参数

1.数组元素可以用做函数实参,不能用做形参。在用数组元素做函数实参时,把实参的值传给形参,是值传递方式。

数据传递的方向是从实参传到形参,单向传递。



2.数组名作函数参数

用数组元素做实参时,向形参变量传递的是数组元素的值,而用数组名作函数实参时,向形参(数组名或指针变量)传递的是数组首元素的地址



3.多维数组名作函数参数

int array[3][10];

或 int array[][10]

7.2 局部变量和全局变量


(一)局部变量

定义在函数内部的变量称为局部变量(Local Variable),它的作用域仅限于函数内部, 离开该函数后就是无效的,再使用就会报错。

定义变量可能有3种情况:

(1)在函数的开头定义

(2)在函数内的复合语句中内定义

(3)在函数的外部定义

int f1(int a)
{
  int b,c;  //a,b,c仅在函数f1()内有效
  return a+b+c;
}
int main()
{
int m,n;  //m,n仅在函数main()内有效
return 0;
}

几点说明:

	1) 在 main 函数中定义的变量也是局部变量,只能在 main 函数中使用;同时,main 函数中也不能使用其它函数中定义的变量。main 函数也是一个函数,与其它函数地位平等。

	2) 形参变量、在函数体内定义的变量都是局部变量。实参给形参传值的过程也就是给局部变量赋值的过程。

	3) 可以在不同的函数中使用相同的变量名,它们表示不同的数据,分配不同的内存,互不干扰,也不会发生混淆。

	4) 在语句块中也可定义变量,它的作用域只限于当前语句块。

	5)形式参数也是局部变量。

(二) 全局变量

在所有函数外部定义的变量称为全局变量(Global Variable),它的作用域默认是整个程序,也就是所有的源文件,包括 .c 和 .h 文件。全局变量可以为本文件中其他函数所共用。它的有效范围为从定义变量的位置开始到本源文件结束。

int a, b;  //全局变量
void func1(){
//TODO:
}

float x,y;  //全局变量
int func2(){
    //TODO:
}

int main(){
//TODO:
return 0;
}

a、b、x、y 都是在函数外部定义的全局变量。C语言代码是从前往后依次执行的,由于 x、y 定义在函数 func1() 之后,所以在 func1() 内无效;而 a、b 定义在源程序的开头,所以在 func1()、func2() 和 main() 内都有效。

建议:不在必要时不要使用全局变量,原因如下:

1)全局变量在程序的全部执行过程中都占用储存单元,而不是仅在需要时才开辟单元。

2)它使函数的通用性降低了。

3)  使用全局变量过多,会降低程序的清晰度。

7.3 变量的储存方式和生存期


变量的存储有两种不同的方式: 静态存储方式和动态存储方式。

1)静态存储方式是指在程序运行期间由系统分配固定的存储空间的方式。

2)动态存储方式则是在程序运行期间根据需要进行动态的分配存储空间的方式。

用户区中,存储空间分为程序区,静态存储区,动态存储区。数据分别存放在静态存储区和动态存储区中。

全局变量全部存放在静态存储区中,在程序开始执行时给全局变量分配存储区,程序执行完毕就释放。

注意:在程序过程中它们占据固定的存储单元,而不是动态地进行分配和释放。

在动态存储区存放以下数据:

(1)函数形式参数。在调用函数时给形参分配存储空间。

(2)函数中定义的未加static声明的变量,即自动变量。

(3)函数调用时的现场保护和返回地址等。每一个变量和函数有两个属性:数据类型和数据的存储类别。

(一)局部变量的存储类别

1. 自动变量(auto变量)

函数中的局部变量,如果不专门声明为static(静态)存储类别,都是动态地分配存储空间的,数据存储在动态存储区中。函数中的形参和在函数中定义的变量,都属于此类。

在调用该函数时,系统会给这些变量分配存储空间,在函数调用结束时就自动释放这些存储空间。这类局部变量称为自动变量。实际上,关键字“auto”可以省略,auto不写则隐含确定为“自动存储类别”,它属于动态存储方式。

2. 静态局部变量(static局部变量)

有时希望函数中的局部变量的值在函数调用结束后不消失而保留原值,即其占用的存储单元不释放,在下一次该函数调用时,该变量已有值,就是上一次调用结束时的值。这是就应该指定该局部变量为“静态局部变量”,用关键字static进行声明。

对于静态局部变量的说明:

(1)静态局部变量属于静态存储类别,在静态存储区内分配单元。在程序整个运行期间都不释放。而自动变量(即动态局部变量)属于动态存储类别,占动态存储类别,占动态存储区空间而不占静态存储区空间,函数调用结束后即释放。

(2)对静态局部变量是在编译时赋初值的,即只赋初值一次,在程序运行时它已有初值。以后每次调用函数时不再重新赋初值而只是保留上传函数调用结束时的值。而对自动变量赋初值,不是在编译时进行的,而是在函数调用时进行,每调用一次函数重新给一次初值,相当于执行一次赋值语句。

(3)如在定义局部变量时不赋初值的话,则对静态局部变量来说,编译时自动赋初值0或空字符。而对自动变量来说,如果不赋初值,则它的值另分配存储单元,而所分配的单元中的值时不可知的。

(4)虽然静态局部变量在函数调用结束后仍然存在,但其他函数是不能引用它的。因为它是局部变量,只能被本函数引用,而不能被其他函数引用。

3.寄存器变量(register变量)

一般情况下,变量(包括静态存储方式和动态存储方式)的值时存放在内存中的。C语言允许将局部变量的值放在CPU中的寄存器中,需要用时直接从寄存器取出参加运算,不必再到内存中去存取。由于对寄存器的存取速度远高于对内存的存取速度,因此这样可以提高执行效率。这种变量叫寄存器变量,用关键字register作声明。

由上可知,三种局部变量的存储位置是不同的:自动变量存储在动态存储区;静态局部变量存储在静态存储区;寄存器变量存储在CPU中的寄存器中。

(二)全局变量的存储类别

全局变量都是存放在静态存储区中的,因此它们的生存期是固定的,存在于程序的整个运行过程。

  1. 一个文件内扩展外部变量的作用域如果外部变量不在文件的开头定义,其有效的作用范围只限于定义处到文件结束。在定义点之前的函数不能引用该外部变量。

如果由于某种考虑,在定义点之前的函数需要引用该外部变量,则应该在引用之前用关键在对该变量作“外部变量声明”,表示把该外部变量的作用域扩展到此位置。

2. 将外部变量的作用域扩展到其他文件

一个C程序可以由一个或多个源程序文件组成。如果程序只由一个源文件组成,使用外部变量。在任一文件中定义外部变量,而在另一个文件中用extern作外部声明。在编译和连接时,系统会由此知道是一个已在别处定义的外部变量,并将在另一个文件中定义的外部变量的作用域扩展到本文件,在本文件中可以合法地使用外部变量。

示例:用extern将外部变量的作用域扩展到其他文件。

3. 将外部变量的作用域限制在本文件中

有时在程序设计中希望某些外部变量只限于本文件中引用,而不能被其他文件引用。这是可以在定义外部变量时加一个static声明。这种加上static声明、只能用于本文件的外部变量称为静态外部变量。

用static声明一个变量的作用是:
(1)对局部变量用static声明,把它分配在静态存储区,该变量在整个程序执行期间不释放,为其分配的空间始终存在。
(2)对全局变量用static声明,则该变量的作用域只限于本文件模块。例如:int a;static a;

7.4 内部函数和外部函数


1.内部函数

如果一个函数只能被本文件中其他函数所调用,它称为内部函数。在定义内部函数时,在函数名和函数类型的前面加static

即:  static 类型名 函数名 (形参表);

例如:函数的首行 static int fun(int a,int b)

表示 fun 是一个内部函数,不能被其他文件调用。

内部函数又称静态函数,使用内部函数,可以使函数的作用域只局限于所有文件。

2.外部函数

如果 在定义函数时,在函数首部的最左端加关键字extern,则此函数是外部函数,可供其他文件调用。

函数首部可以为

extern int fun(int a,int b)

fun就可以被其他文件调用。c语言规定,如果在定义函数时省略extern,则默认为外部函数。

举例:

//file1.c 文件1
#include<stdio.h>
int main()
{
    extern void enter_string(char str[]);
    extern void delete_string(char str[],char ch);
    extern void print_string(char str[]);
    char c,str[80];
    enter_string(str);
    scanf("%c",&c);
    delete_string(str,c);
    print_string(str);
    return 0;
}
//file2.c 文件2
void enter_string(char str[80])  //定义外部函数 enter_string
{
    gets(str);  // 向字符数组输入字符串
}
//file3.c 文件3
void delete_string(char str[],char ch)
{
    int i,j;
    for(i=j=0;str[i]!='\0';i++)
        if(str[i]!=ch)
        str[j++]=str[i];
    str[j]='\0';
}
//file4.c 文件4
void print_string(char str[])
{
    printf("%s\n",str);
}


8.1 指针


(一)指针和指针变量

指针是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址。地址指向该变量单元。即将地址形象地称为指针。

一个变量的地址称为该变量的指针。

如果有一个变量专门用来存放另一变量的地址(即 指针),它称为“指针变量“。指针变量就是指针变量,用来存放地址,指针变量的值为地址(即指针)。

指针是一个地址,而指针变量是存放地址的变量。

(二)定义指针变量

定义指针变量的一般形式为:

类型名  *指针变量名;

如:

int  *pointer_1,*pointer_2;

float *pointer_3,;

char *pointer_4;

可以在定义指针变量时,同时对它初始化。

int *pointer1=&a;定义指针变量pointer_1;
注意:
1. 指针变量“ * ”表示该变量的类型为指针型变量(间接访问)
2.在定义指针变量时必须指定基类型。
3.一个变量的指针的含义包含两方面,一是以存储单元编号表示的地址(如编号为2000的字节),一是它指向存储单元的数据类型(如 int ,char,float)等
4.指针变量中只能存放地址,不要将一个整数赋给一个指针变量。如*pointer_1=100 (不合法)

(三)引用指针变量

在引用指针变量时,可能有三种情况:
(1)给指针赋值
如: p=&a;
指针变量p的值为变量a的地址,p指向a。

(2)引用指针变量指向的变量
如果 p=&a;
printf(”%d“,*p);
*p=1;
则 a=1;

(3)引用指针变量的值
如:

printf(“%d”,p);

作用是以八进制数形式输出指针变量p的值,如果p指向了a,就是输出a的地址,即&a。

(四)指针变量作为函数参数。

函数参数不仅可以是整型、浮点型、字符型等数据,还可以是指针类型。它的作用是将一个变量的地址传送到另一个函数中。

举例:

#include<stdio.h>
int main()
{
    void swap(int *p1,int *p2);
    int a,b;
    int *pointer_1,*pointer_2;
    printf("please enter  a and b:");
    scanf("%d,%d",&a,&b);
    pointer_1=&a;
    pointer_2=&b;
    if(a<b) swap(pointer_1,pointer_2);

    printf("max=%d,min=%d\n",a,b);
    return 0;
 } 
 void swap(int *p1,int *p2)
 {
     int  temp;
     temp=*p1;
     *p1=*p2;
    *p2=temp; 
  } 

8.2 通过指针引用数组


(一)数组元素的指针

(1)所谓数组元素的指针就是数组元素的地址。可以用一个指针变量指向一个数组元素。例如:

int a[10]={1,3,5,7,8,9,,412,154,52,,3234};

int  *p;

p=&a[0];//把a[0]元素的地址赋给指针变量p

(2)在c语言中,数组名代表数组中首元素的地址。因此,下面两个语句等价:

p=&a[0];

p=a;

(3)在定义指针变量时可以对它初始化,如:

int *p=&a[0];

(二) 引用数组元素时指针的运算

在指针指向数组元素时,可以对指针进行以下运算:

1)加一个整数(用+或+=),如p+1;

2)减一个整数(用-或-=),如p-1;

3)自加运算,如p++, ++p;自减运算,如p--,--p。

4)两个指针相减,如p1-p2(只有p1和p2都指向同一个数组中的元素时才有意义)
(1)如果指针变量p已指向数组中的一个元素,则p+1指向同一个数组中的下一个元素,p-1指向同一个数组中的上一个元素。执行p+1时并不是将p的值(地址)简单地加1,而是加上一个数组元素所占用的字节数。例如,数组元素是float型,每个元素占用4个字节,则p+1意味着使p的值加4个字节,以使它指向下一个元素。

(2)(p+i)或(a+i)是p+i或a+i所指向的数组元素,即a[i]。例如,(p+5)或(a+5)就是a[5],即这三者等价。

(3)如果指针变量p1和p2都指向同一数组,如执行p2-p1,结果是p2-p1的值(两个地址之差)除以数组元素的长度。人们就不需要p1和p2得值,只需去计算所指元素的相对距离,两个地址不能相加,如p1+p2是无实际意义的。

(三)通过指针引用数组元素

(1)下标法。如a[i]形式

(2)指针法,如*(a+i)或    *(p+i)

(四)通过指针引用多维数组

	1.int a[3] [4]={{1,3,5,7},{9,11,13,15},{17,19,21,23}};
表示形式           		含义             				地址                                                                                    		

  a     二维数组名,指向一维数组a[0],即0行首地址     		2000                            

a[0], * (a+0), * a     0行0列元素地址                     2000              

a+1,&a[1]             1行首地址                          2016

a[1], *(a+1)         1行0列元素  a[1][0]的地址           2016

a[1]+2, * (a+1)+2, &a[1][2]    1行2列元素a[1][2]的地址     2024

*(a[1]+2), *(*(a+1)+2), a[1][2] 1行2列元素 a[1][2]的值  元素值为13                                             

8.3 指针引用字符串 以及 字符指针变量和字符数组的比较


(一)字符串的引用方式

(1)用字符数组存放一个字符串,可以通过数组名和下标引用字符字符串中一个字符,也可以通过数组名和格式声明“%s”输出该字符串。

举例:

#include<stdio.h>
int main()
{
    char string[]="I love China!";
    printf("%s\n",string);
    printf("%c\n",string[7]);
    return 0;
 } 

(2)用指针变量访问字符串。通过改变指针变量的值使它指向字符串中的不同字符。

(二)使用字符指针变量和字符数组的比较

(1)字符数组有若干个元素组成,每隔元素中放一个字符,而字符指针变量中存放的是地址(字符串第一个字符的地址),绝不是将字符串放到字符指针变量中。

(2)赋值方式:可以对字符指针变量赋值,但对不能对数字名赋值。

     可以采用下面方法对字符指针变量赋值:

       char *a;                   //a为字符指针变量
     
     a = "I love China";      //将字符串首元素地址赋给指针变量,合法。但赋给a的不是字符串,而是字符串第一元素的地址。   

不能用以下办法对字符数组名赋值:

char str[14];
str[0] = 'I';                    //对字符数组元素赋值,合法!
str = "I love China";       //数组名是地址常量,不能被赋值,非法!

(3)初始化的含义,对字符指针变量赋初值:

char *a = “I love China”; //定义字符指针变量a,并把字符串第一个元素的地址赋给a等价于:

char *a;                   //定义字符指针变量aa = "I love China";        //把字符串第一个元素的地址赋给a

而对数组的初始化:

char str[14] = "I love China"; //定义字符数组str,并把字符串赋给数组中各元素。

不等价于:

char str[14];                //定义字符数组strstr = "I love China";        //企图把字符串赋给数组中各元素,错误

数组可以在定义时对各元素赋初值,但不能用赋值语句对字符数组中全部元素整体赋值。

(4)存储单元的内容。

    编译时为字符数组分配若干存储单元,以存放各元素的值,而对字符指针变量,只分配一个存储单元(Visual C++为指针变量分配4个字节)

(5)指针变量的值是可以改变的,而数组名代表一个固定的值(数组元素的地址),不能改变。

char *a = "I love China";a = a + 7;                //改变指针变量的值,即改变指针变量的指向printf("%s\n", a);        //输出从a指向的字符开始的字符串

char str[] = "I love China";str = str + 7;            //数组名虽然代表地址,但它是常量,值不能改变。不合法。

(6)字符数组中各元素的值是可以改变的(可以对它们再赋值),但是字符指针变量指向的字符串常量中的内容是不可以被取代的(不能再赋值)。

char a[]="House";   //字符数组a初始化
char *b="House"//字符指针变量b指向字符串的第一个字符

a[2]='r';//合法,r取代a数组元素a[2]的原值u
b[2]='r';//非法,字符常量不能改变

(7)引用数组元素。
字符数组可以用下表法(用数组名和下表)引用一个数组元素(如a[5]),也可以用地址法(如*(a + 5))。

如果定义了字符指针变量p,并使他指向数组a的首元素,则可以用指针变量带下表的形式引用数组元素(如p[5]),地址法(如*(p + 5))引用数组元素a[5]


8.4 函数指针


8.4.1 函数指针

如果在程序中定义了一个函数,在编译时,编译系统为函数代码分配一段储存空间,这段储存空间的起始地址(又称入口地址)称为这个函数的指针。

例如: int (*p)(int,int);

定义p是一个指向函数的指针变量,它可以指向函数的类型为整型且有两个整型参数的函数。

p的类型用int (*)(int,int)表示

8.4.2 用函数指针变量调用函数

(1)通过函数名调用。

(2)通过指向函数的指针变量来调用该函数。

8.4.3 定义和使用指向函数的指针变量

一般形式:

类型名 (*指针变量名)(函数参数比表列)

如: “int*p)(intint)”,这里的类型名是指函数返回值的类型。

说明:

  (1) 指针变量只能指向在定义时指定的类型的函数。
  (2) 如果要用指针变量调用函数,必须先使用指针变量指向该函数。如:
  p=max;这就把max函数的入口地址赋给了指针变量p。
  (3)在给函数指针变量赋值时,只须给出函数名而不必给参数。
  (4)用函数指针变量调用函数时,只须将(*p)代替函数名即可(p为指针变量名),在 (*p)之后的括号中根据需要写上实参。

例如:

c=*p)(a,b);// 表示调用由p指向的函数,实参为a,b,得到函数值赋给c。
(5)对指向函数的指针变量不能进行算术运算,如p+n,p++,p--
(6)用函数名调用函数,只能调用所指定的一个函数,而通过指针变量调用函数比较灵活,可以根据不同情况先后调用不同的函数。

8.4.4 用指向函数的指针作函数参数

指向函数的指针变量的一个重要用途是把函数的地址作为参数传递到其他函数。

指向函数的指针可以作为函数参数,把函数的入口地址传递给形参,这样就能够在被调用的函数中使用实参函数。

例如:

实参函数名  

void  fun (int*x1)(int),int*x2)(intint))
{
int a,b,i=3,j=5;
a=* x1)(i);
b=*x2)(i,j);
}

8.4.5 返回指针值的函数

定义返回指针值的函数的一般定义形式为:
类型名 *函数名 (参数表列);


8.5 动态内存分配与指向它的指针变量以及 数组指针和指针数组


讲动态内存分配之前,我们要引入一些内存相关知识。

8.5.1 C语言内存分配区域

C语言在内存中一共分为5个区域,分别是:

  1. 栈区: 存放局部变量名
  2. 堆区: 存放new或者malloc出来的对象
  3. 常数区: 存放局部变量或者全局变量的值
  4. 全局静态区: 用于存放全局变量或者静态变量
  5. 代码区: 二进制代码

主要注意栈区和堆区

栈区:由编译器自动管理内存的创建和销毁情况,函数内定义的变量、函数的形式参数、函数调用过程时的内存开销都位于栈区。

堆区:由程序手动管理内存的创建和销毁情况,使用malloc函数动态分配的内存存放于堆区。

8.5.2 动态内存分配的含义

C语言允许建立动态内存分配区域,以存放一些临时用的数据,这些数据不必再程序的声明部分定义,也不必等到函数结束时才释放,而是要随时开辟,不需要随时释放,这些数据是临时存放在一个特定的自由存储区(堆),可以根据需要向系统申请所需要大小的空间,由于未在声明部分定义它们为变量或数组,因此不能通过变量名或数组名去引用这些数据,只能通过指针来引用。

8.5.3 建立内存的动态分配

对内存的动态分配是通过系统提供的函数库来实现的,主要有malloc、calloc、free、realloc这四个函数,注意头文件为stdlib.h

1)使用malloc函数

      其函数原型为void *malloc(unsigned int size);
      其作用是在内存的动态存储区域中分配一个长度为size的连续空间,形参size的类型定义为无符号整形(不允许为负数)。此函数的值(即返回值)是所分配区域的第一个字节的地址,或者说,此函数是一个指针型函数,返回的指针指向该分配区域的开头位置,如:
       malloc(100)//开辟100字节的临时分配区域,函数值为其第一个字节的地址
       struct good *head=(struct good*)malloc (sizeof(struct goods))

注意:

其指针的基类型为void,即不能执行任何类型的数据,只能提供一个地址。如果此函数未能成功执行(例如内存空间不足),则返回空指针(NULL)

更形象的描述一下(如下图):

 int *p=int *malloc(sizeof(int))

在这里插入图片描述

 int *p=int*)malloc(5*sizeofint))//申请20个连续的字节

此时相当于数组
在这里插入图片描述

(1)若只分配一个元素的内存空间,则直接使用*p表示这一块的内存空间的内容

  int*p=int*malloc(sizeof(int));
  *p=3;

(2)若分配多个元素的内存空间,可以使用p、(p+i)指针的形式访问,也可以使用数组形式p[0]、p[i]形式存取内容。

2)使用calloc函数

  其函数原型为

   void *calloc(unsigned n, unsigned size);//其作用是在内存的动态区域中分配n个长度为size的连续空间,这个空间一般比较大,足以保存一个数组。
                
用calloc函数可以为以为数组来开辟动态存储空间,n为数组元素的个数,每个元素的长度为size。这就是动态数组,函数返回所分配区域的其实位置指针;如果分配不成功,返回NULL。如

    p = calloc(50,4;//开辟50X4个字节的临时分配区域,把起始地址赋给指针变量p

3)使用free函数

 其原型为
        void free(void *p);
    其作用是释放指针变量p所指的动态空间,使这部分空间能重新被其他变量使用,p应该是最近一次调用calloc或malloc函数得到的函数返回值,如;
                 free(p) //释放指针变零p所指向的已知的分配的动态空间
                free函数值返回值

4)使用realloc函数

   其原型为
        void *realloc(void *p, unsigned int size);
   //如果已经通过malloc函数或calloc函数获得了动态空间,向改变其大小,可以用realloc函数重新分配。

   用realloc函数将p所指向的动态空间的大小改变为size,p的值不变,如果重新分配不成功,返回NULLrealloc(p,50) //将所指的已分配的动态空间该为50个字节

  以上4个函数的声明在stdlib.h头文件中,在用到这些函数的时候应用#“include<stdlib.h>”指令把stdlib.h头文件包含到程序文件中

3.void指针类型

  C99允许使用基类型为void的指针类型。可以定义一个基类型为void的指针变量(即void *型变量),它不指向任何类型的数据。

把void指针赋给不同基类型的指针变量(或相反)时,编译系统会自动的进行转换,不必用户自己进行强制类型转换,如 :

void *p1; int a = 3;p1 = &a.相当于p1 = (void*)&a;

例题:建立动态数组,输入5个学生的成绩,另外用一个函数检查其中有无低于60分的,输出不及格的成绩。

#include<stdio.h>
#include<stdlib.h>
 int main()
  {
      void check(int *);
      int *p1;     //开辟动态内存区,将地址转换为int *型,然后放在p1中 
     p1 = (int *)malloc(5* sizeof(int));   //p1 = malloc(5*sizeof(int));也可以
     for (int i = 0; i < 5; i++)
     scanf_s("%d",p1+i);//输入5个学生的成绩
     check(p1);
     return 0;
 }
 void check(int *p)
 {
     int i;
     for (int i = 0; i < 5;i++)
     if (p[i] < 60) printf("%d ",p[i]);
     printf("\n");
    }

8.5.4 数组指针和指针数组

优先级:()>[]>*

(1)数组指针

 定义` int (*p)[n];`
()优先级高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长。也就是说执行p+1时,p要跨过n个整型数据的长度。

如要将二维数组赋给一指针,应这样赋值:

int a[3][4];
int (*p)[4]; //该语句是定义一个数组指针,指向含4个元素的一维数组。
p=a;        //将该二维数组的首地址赋给p,也就是a[0]或&a[0][0]
p++;       //该语句执行过后,也就是p=p+1;p跨过行a[0][]指向了行a[1][]

所以数组指针也称指向一维数组的指针,亦称行指针。

(2)指针数组

定义 int *p[n];

[]优先级高,先与p结合成为一个数组,再由int*说明这是一个整型指针数组,它有n个指针类型的数组元素。

这里执行p+1是错误的,这样赋值也是错误的:p=a;因为p是个不可知的表示,只存在p[0]、p[1]、p[2]…p[n-1],而且它们分别是指针变量可以用来存放变量地址。

但可以这样 *p=a; //这里*p表示指针数组第一个元素的值,a的首地址的值

如要将二维数组赋给一指针数组:

int *p[3];
int a[3][4];
for(i=0;i<3;i++)
p[i]=a[i];//这里int *p[3] 表示一个一维数组内存放着三个指针变量,分别是p[0]、p[1]、p[2]     
//所以要分别赋值

数组指针只是一个指针变量,是C语言里专门用来指向二维数组的,它占有内存中一个指针的存储空间。指针数组是多个指针变量,以数组形式存在内存当中,占有多个指针的存储空间。

还需要说明的一点就是,同时用来指向二维数组时,其引用和用数组名引用都是一样的。
比如要表示数组中i行j列一个元素:

*(p[i]+j)*(*(p+i)+j)(*(p+i))[j]、p[i][j]

9. 结构体与结构体指针


9.1 自己建立结构体类型

结构体与数组类似,都是由若干分量组成的,与数组不同的是,结构体的成员可以是不同类型,可以通过成员名来访问结构体的元素。

C语言允许用户自己建立有不同类型数据组成的组合型的数据结构,称为结构体。

结构体的定义说明了它的组成成员,以及每个成员的数据类型。定义一般形式如下:

struct  结构类型名 
{ 
数据类型 成员名 1; 
数据类型 成员名 2; 
...... 
数据类型 成员名 n; 
}; 

说明:

1)结构体类型并非只有一种,而是可以设计出许多种结构体类型。
2)成员可以属于另一个结构体类型。

9.2 定义结构体类型变量
(1)先声明结构体类型,在定义该类型的变量

struct 结构体类型名 结构体变量名;

例如:struct Student student 1, student 2

(2)在声明类型的同时定义变量

struct 结构体名{
成员表列

} 变量名表列;

(3)不指定类型名而直接定义结构体类型变量

其一般形式为
struct{
成员表列

} 变量名表列;

9.3 结构体变量的初始化和引用

在定义结构体变量时,可以对它初始化,即赋予初始值。然后可以引用这个变量,例如输出它的成员的值。

(1)在定义结构体变量时可以对它的成员初始化。初始化列表是用花括号括起来的一些常量,这些常量依次赋给结构体变量中各成员。
例如:struct Student b={.name=“Zhang Fang”};

(2)可以引用结构体变量中成员的值,引用方式为:

结构体变量名 成员名

例如:
student1.num表示student1 变量中的num成员,即student1的num(学号)成员

在程序中可以对变量的成员赋值,例如:student1.num=10010;

“.”是成员运算符,它在所有的运算符中优先级最高。

注意不能企图输出结构体变量名来到达输出结构变量所有成员的值。

(3)如果成员本身又属一个结构体类型,则要用若干个成员运算符,一级一级地找到最低级的一级的成员。只能对最低级的成员进行赋值或存取以及运算。

(4)对结构体变量的成员可以向普通变量一样进行各种运算

student2.score=student1.score;
sum=student1.score+student2.score;
student1.age++;(由于"."的运算符等级最高,student1.age进行自加运算)


(5)同类的结构体变量可以互相赋值,如:student1=student2;


(6)可以引用结构体变量成员的地址,也可以引用结构体变量的地址。例如:

scanf(“%d”,&student1.num);
printf(“%o”,&student1);(输出结构体变量&studnet1的首地址)

9.4 定义结构体数组

(1)一般形式为:

 1.  struct 结构体名

{成员表列} 数组名  [数组长度]
 2.  先声明一个结构体类型(如struct person),然后再 用此定义结构体数组:
        结构体类型  数组名 [数组长度]
   如:
    struct person leader[3];

(2)对结构体数组初始化的形式是在定义数组的后面加上:
={初值列表}

如:struct person leader[3]={"Li",0,“Zhang”,0,“Sum”,0};

9.5 结构体指针

(1)指向结构体变量的指针

指向结构体对象的指针变量即可以指向结构体变量,也可以指向结构体数组中的元素。指针变量的基类型必须与结构体变量的类型相同。

例如:

struct Student *pt;//pt可以指向struct Student 类型的变量或数组元素
为了方便和直观,允许把(*p).num用p->num 来代替,“->”代表一个箭头。

如果p指向一个结构体变量stu,以下3种用法等价:

1.   stu.成员(如 stu.num)

2.*p).成员名(如(*p).num);

3.   p->成员名(如p->num)

(2) 指向结构体数组的指针

指针变量可以指向一个结构数组,这时结构指针变量的值是整个结构数组的首地址。结构指针变量也可以指向结构数组的一个元素,这时的结构指针变量的值是该结构数组元素的首地址。

*ps为指向结构数组的指针变量,则ps也是指向该结构数组的0号元素,ps+1指向1号元素,ps+i则指向i号元素

在程序中定义了stu结构类型的外部数组boy并做了初始化赋值。在main中定义了ps为指向stu类型的指针。在循环语句for的表达式1中,ps被赋予boy的首地址,然后循环5次,输出boy中数组中的成员值。

用指针变量输出结构数组

#include<stdio.h>
struct stu
{
    int num;
    char name[8];
    char sex;
    float score;
}boy[5] = {
        {101,"shi",'w',65},
        {102,"wen",'m',89},
        {103,"jie",'w',86},    
        {104,"jie jie",'m',70},
        {105,"hi",'m',76},
        
};
int main(int argc,char **argv)
{
    struct stu *ps;
    printf("num\tname\tsex\tscore\n");
    for(ps=boy;ps<boy+5;ps++)
    {
        printf("%d\t%s\t%c\t%f\n",ps->num,ps->name,ps->sex,ps->score);
    }
    return 0;
    } 

(3)结构体的定义与引用

#include <iostream>
#include <algorithm>
using namespace std;
#define edg students
struct Student {
	int id,s;
	char name[10000];
} students[3];
bool cmp(Student a,Student b) {
	return a.s>b.s;
}
int main() {
	for(int i = 0 ; i < 3; i++) {
		cin>>edg[i].id>>edg[i].name>>edg[i].s;
	}
	sort(students,students+3,cmp);
	for(int i = 0 ; i < 3 ; i++) {
		cout<<students[i].id<<students[i].name<<students[i].s;
	}
	return 0;
}

(4)结构体变量作为函数参数

#include<iostream>
using namespace std;
struct date {
	int year;
	int month;
	int day;

};
void pdate(struct date p) {
	p.year=2014;
	p.month=5;
	p.day=20;
};
int main() {
	struct date d;
	d.year=2013;
	d.month=4;
	d.day=22;
	cout<<d.year<<d.month <<d.day<<endl;
	pdate(d);
	cout<<d.year<<d.month<<d.day<<endl;
	return 0;
}

10. 用指针处理链表


10.1 链表的定义

链表是一种物理存储结构上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

一种常见的数据结构。它是动态地进行储存分配的一种方式。

链表有一个“头指针”变量,它存放一个地址,该地址指向一个元素。每个结点都应该包括两个部分:

(1)用户需要用的实际数据
(2)下一个结点的地址

在这里插入图片描述
在这里插入图片描述

10.2 链表的分类

在这里插入图片描述

10.3. 动态链表

(1)使用动态链表存储数据,不需要预先申请内存空间,而是在需要的时候才向内存申请。即动态链表存储数据元素的个数是不限的,想存多少就存多少。
(2)使用动态链表的整个过程,只需操控一条存储数据的链表。当表中添加或删除数据元素时,你只需要通过 malloc 或 free 函数来申请或释放空间即可,实现起来比较简单。
(3)相对于静态链表的“确定节点个数”,动态链表则是没有确定的节点个数,需要在创建过程中根据一些特定条件(例如节点中成员的值为空则不再创建)而控制创建。
(4)创建动态链表时,无需先输入一个节点个数了,而是在创建过程中根据创建条件创建,只要符合条件,则是一直创建,否则退出创建并打印。
举例:
#include <stdio.h>
#include <stdlib.h>

#define LEN sizeof(struct Student)

typedef struct Student{
	int num;
	char name[10];
	struct Student *next;
}stu;

// 创建头节点 
struct Student *creat()
{
	stu *head;
	head = (stu *)malloc(LEN);
	if(head == NULL){
		printf("申请失败");
	} else {
		head->next == NULL;
	}
	return head;
}

// 初始化数据
void init_data(stu *p)
{
	int n,i;
	stu *q = NULL;
	printf("请输入要添加的学生的个数: ");
	scanf("%d", &n);
	for(i = 0; i< n; i++){
		q = (struct Student *)malloc(LEN);			//寻找符合条件的结点 
		printf("请输入第%d个学生的学号\n", i+1);
		scanf("%d", &q->num);
		printf("请输入第%d个学生的姓名\n", i+1);
		scanf("%s", q->name);
		p->next = q; 								// 将q赋值给p的后继 
		p = p->next;  								// p向后移 
	}
	q->next = NULL; 
} 

// 添加数据 
/*
	1. 声明一个指针q指向表头第一个存放数据的结点
	2. 当q->num != id时,就让q指针向后移动,不断指向下一个结点
	3. 若找到链表末尾q为空,则说明num == id的节点不存在 
	4. 否则查找成功,在系统中生成一个空结点Q,并赋值
	5. 单链表插入的标准语句: Q->next = q->next;	q->next = Q;,插入完成后让flag=true 
	6. 如果添加失败即flag==false,输出提示信息	
*/ 
void add_data(stu *p, int id)
{
	stu *q = p->next;
	stu *Q = NULL;
	bool flag = false;

	while(q){
		if(q->num == id){ 							//寻找符合条件的结点 
			Q = (struct Student *)malloc(LEN); 		// 生成新的结点 
			printf("请输入要插入的学生的编号:");
			scanf("%d", &Q->num);
			printf("请输入要插入的学生的姓名:");
			scanf("%s", Q->name);	
			Q->next = q->next;						//将q的后继结点赋值给Q的后继 
			q->next = Q;							// 将Q赋值给q的后继 
			flag = true;
		}	
		q = q->next;								// 指针后移 
	}
	if(flag == false){
		printf("该编号不存在,插入失败\n");
	}
}
 
// 删除数据
/*
	1. 声明一个指针q接收表中第一个有数据的结点,l为q结点前面的结点
	2. 当q->num != id时,就让q指针不断向后移,不断指向下一个结点,同时l也随之向后移动
	3. 若找到链表末尾q为空,则说明num == id的结点不存在
	4. 若查找成功,则执行删除语句
	5. 单链表删除的语句为: l->next = q->next; free(q); 删除完成后,使flag=true 
	6. 如果删除失败即flag==false,输出提示信息 
*/ 
void delete_data(stu *p)
{
	stu *q = p->next;
	stu *l = p;
	bool flag = false;
	int id;
	printf("请输入要删除的学生的学号: ");
	scanf("%d", &id);
	while(q){
		if(q->num == id){						//寻找符合条件的结点 
			l->next = q->next;					//将q的后继赋值给l的后继,(即把q节点省略过去) 
			free(q);							//让系统回收此节点,释放内存 
			flag = true;
			break;
		}
		q = q->next;							//指针后移 
		l = l->next;
	}
	if(flag == false){
		printf("未查询到该学生,删除失败\n");
	}
} 

// 查询数据
/*
	1. 声明一个指针q接收第一个存放数据结点
	2. 定义变量输入我们要查询的结点
	3. 若q->num != id,就遍历链表,,让q指针不断后移,不断指向下一个结点
	4. 若到链表末尾q为空,则说明这个结点不存在 
	5. 否则查找成功,输出节点q的数据 ,查找成功 使flag=true
	6. 查找失败即flag==false,输出提示信息 
*/ 
void find_data(stu *p)
{
	stu *q = p->next;
	bool flag = false;
	int id;
	printf("请输入要查询的学生的编号:");
	scanf("%d", &id);
	while(q){
		if(q->num == id){
			printf("%d %s\n", q->num, q->name);
			flag = true;
		}
		q = q->next;
	}
	if(flag == false){
		printf("未查询到该学生\n");
	}

} 
 
// 修改数据
/*
	1. 定义指针q
	2. 定义变量输入 我们要修改的结点
	3. 若q->num != id,就遍历链表,让q不断向后移,不断指向下一个结点
	4. 若到链表末尾q为空,则说明这个结点不存在
	5. 否则查找成功,修改q结点的内容,使flag=true
	6. 查找失败即flag==false,输出提示信息 
*/ 
void change_data(stu *p)
{
	stu *q = p->next;
	bool flag = false;
	int id;
	printf("请输入要修改的学生的学号:");
	scanf("%d", &id);
	while(q){
		if(q->num == id){
			printf("请输入修改后的编号:");
			scanf("%d", &q->num);
			printf("请输入修改后的姓名:");
			scanf("%s", q->name);
			flag = true;
		}
		q = q->next;
	}
	if(flag == false){
		printf("未查询到该学生,修改失败\n");
	}	
} 
// 遍历链表
/*
	1. 定义指针q
	2. 如果q不为空,输出结点里的数据,并让指针q向下移,不断指向下一个结点 
	3. 否则结束遍历,遍历完毕 
*/
void traverse(stu *p)
{
	stu *q = p->next;
	while(q != NULL){
		printf("%d %s\n", q->num, q->name);
		q = q->next;
	}
} 
int main()
{
	stu *p;
	int id;
	p = creat();
	init_data(p);
	traverse(p);
//	delete_data(p);
	printf("请输入要插入的位置: ");
	scanf("%d", &id);
	add_data(p, id);
//	find_data(p);
//	change_data(p);
	traverse(p);
	return 0;
}

10.4 动态链表的增删查改与排序

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

#define LEN sizeof(struct Student)

typedef struct Student{
	int num;
	char name[10];
	struct Student *next;
}stu;
// 创建头节点 
struct Student *creat()
{
	stu *head;
	head = (stu *)malloc(LEN);
	if(head == NULL){
		printf("申请失败");
	} else {
		head->next == NULL;
	}
	return head;
}

// 初始化数据
void init_data(stu *p)
{
	int n,i;
	stu *q = NULL; // 申请空间	
	printf("请输入要添加的学生的个数: ");
	scanf("%d", &n);
	for(i = 0; i< n; i++){
		q = (struct Student *)malloc(LEN);
		printf("请输入第%d个学生的学号\n", i+1);
		scanf("%d", &q->num);
		printf("请输入第%d个学生的姓名\n", i+1);
		scanf("%s", q->name);
		p->next = q;
		p = p->next; 
	}
	q->next = NULL;
} 
// 遍历链表
void traverse(stu *p)
{
	stu *q = p->next;
	while(q != NULL){
		printf("%d %s\n", q->num, q->name);
		q = q->next;
	}
} 
int main()
{
	stu *p;
	p = creat();
	init_data(p);
	traverse(p);
	return 0;
}

10.4.1 链表的创建

struct goods *creathead()//建立  
 {
 	struct goods *head;
 	head=(struct goods *)malloc(sizeof(struct goods ));
 	if(head==NULL)
 	{
 		exit(0);
	}
	else
	{
		head->next==NULL;
		return head;
	}
}
#include<stdio.h>
#include<stdlib.h>
struct stu{
	int num;
	char name[10];
	struct stu *next;
};
void create(struct stu *head,int n)
{
	struct stu *p1=head;
	for(int i=0;i<n;i++){
		struct stu *p2=(struct stu *)malloc(sizeof(struct stu));
		printf("请输入第%d个学生的num和姓名:",i+1);
		scanf("%d",&p2->num); 
		scanf("%s",p2->name);	
		p1->next=p2;
		p2->next=NULL;
		p1=p2;
	}
}
void print(struct stu *head)
{
	//system("cls");
	printf("所有的学生的信息为:\n");
	printf("num\tname\n");
	struct stu *p1=head;
	while(p1->next!=NULL){
		printf("%d\t%s\n",p1->next->num,p1->next->name);
		p1=p1->next; 
	}
}
int main()
{
	int n;
	struct stu *head;
	head=(struct stu *)malloc(sizeof(struct stu));
	head->next=NULL;
	printf("请输入您要创建的学生的数量:");
	scanf("%d",&n);
	create(head,n);
	print(head);
	return 0;
}

10.4.2 链表的增加

void addlist(goods *head)//增加 
 {
 	goods *p1,*p2;
 	p1=p2=head;
 	while(p1->next!=NULL)
	 {
 		p1=p1->next;
	 }
	p2=(goods*)malloc(sizeof(goods));
	scanf("%5d %5d %5s %5d %5d",&p2->num,&p2->price,&p2->name,&p2->amount,&p2->kg);
	p2->next=NULL;
	p1->next=p2;
}

10.4.3 链表的删除

void deletelist(goods *head)//删除 
{
	int c;
	printf("请输入你要删除的货物编号:\n");
	scanf("%d",&c);
	goods*p1=head;
	goods*p2=head;
	while(p1!=NULL)
	{
		if(p1->num==c)
		{
			p1->next=p2->next;
			free(p1);
			printf("删除成功!\n");
			break; 
		}
		p2=p2->next;
		p1=p2->next;
	}	 
}

10.4.4 链表的查询

void inquiry(goods *head)//查询   
 {
 	
 	int b; 
 	goods *p;
 	p=head;
 	printf("请输入你要查询的货物编号:\n"); 
 	scanf("%d",&b);
 	while(p->num!=b)
 	{
 	p=p->next;
    }
    printf("----------查询结果----------");
	printf("货物编号%d,货物价格%d,货物名称%s,货物数量%d,货物重量%d:\n",p->num,p->price,p->name,p->amount,p->kg);
}

10.4.5 链表的修改

 void change(goods *head)//修改   
 {
 	int a; 
	printf("----------请输入你要修改的编号:");
	scanf("%d",&a);
	goods *p;
	p=head->next;
	while(1)
	{
		if(p->num==a)
		{
			break;	
		}
		p=p->next;
	} 
	scanf("%d %d %s %d %d", &p->num,&p->price,&p->name,&p->amount,&p->kg);
 }

10.4.6 链表的输出

void print(goods *head)//输出  
{
	goods *p;
	p=head->next;
	while(p->next!=NULL)
	{
		printf("货物信息:货物编号%5d,货物价格%5d,货物名称%5s,货物数量%5d,货物重量%5d\n",p->num,p->price,p->name,p->amount,p->kg); 
		p=p->next; 
	}
} 

10.4.7 链表的排序

void sort(goods*head)//排序 
{
 goods *p1,*p2,*p3;
 int x,i;
 int t;
 char name_[20];
 int num,amount,kg;
 for(x=0,p3=head->next;p3!=NULL;p3=p3->next)
 {
	x++;
}
for(i=0;i<x;i++){
		for(p1=head->next,p2=head->next->next;p1!=NULL,p2!=NULL;p1=p1->next,p2=p2->next){
				if(p1->price<p2->price){
						t =p2->price;
						p2->price=p1->price;
						p1->price=t;
						
						strcpy(name_,p2->name);
						strcpy(p2->name,p1->name);
						strcpy(p1->name ,name_);
						
						num=p2->num;
						p2->num=p1->num;
						p1->num=num;
						
						amount=p2->amount;
						p2->amount=p1->amount;
						p1->amount=amount; 
						
						kg=p2->kg;
						p2->kg=p1->kg;
						p1->kg=kg; 
					}
			} 
	}
	goods *p;
	p=head->next;
	while(p->next!=NULL)
	{
		printf("货物信息:货物编号%5d,货物价格%5d,货物名称%5s,货物数量%5d,货物重量%5d\n",p->num,p->price,p->name,p->amount,p->kg); 
		p=p->next; 
	}
}

11. 用typedef声明新类型名

1.简单地用一个新类型名代替原有的类型名

typedef int Integer   //指定用Integer 为类型名 ,作用与int 相同。
typedef float Real;//指定Real为类型名,作用与float相同。

2.命名一个简单的类型名代替复杂的类型表示方法

1)

typedef struct {
    int month;
    int day;
}Date; //  声明了新的类型名Date,然后可以用新的类型名Date去定义变量。

2)命名一个新的类型名代表数组类型

typedef int Num[100];    //声明Num为整形数组类型名
Num a;            //定义a为整形数组名,它有100个元素

3)命名一个新的类型名代表指针类型

typedef char* String;    //声明String为字符指针类型
String p;        //定义p为字符指针变量

4)命名一个新的类型名代表指向函数的指针类型

typedef int (* Pointer)();    //声明Pointer为指向函数的指针类型,函数返回整型值
Pointer p;                   //p为Pointer类型的指针变量
归纳起来,声明一个新的类型名的方法是:
①先按定义变量的方法写出定义体(如int i;)。
②将变量名换成新类型名(例如:将i换成Count)。
③在最前面加typedef(例如:typedef int Count)。
④然后可以用新类型名去定义变量。

11. 文件的打开和关闭 文件的读写


文件操作标准库函数有:

文件的打开操作 fopen 打开一个文件

文件的关闭操作 fclose 关闭一个文件

文件的读写操作 fgetc 从文件中读取一个字符
              fputc 写一个字符到文件中去
              fgets 从文件中读取一个字符串
              fputs 写一个字符串到文件中去
              fprintf 往文件中写格式化数据
              fscanf 格式化读取文件中数据
              fread 以二进制形式读取文件中的数据
              fwrite 以二进制形式写数据到文件中去
              getw 以二进制形式读取一个整数
              putw 以二进制形式存贮一个整数

文件状态检查函数 feof 文件结束
              ferror 文件读/写出错
              clearerr 清除文件错误标志
              ftell 了解文件指针的当前位置

文件定位函数 rewind 反绕
              fseek 随机定位

11.1 文件名

文件标识常被称为文件名

文件标识包括三部分:

(1)文件路径
(2)文件名主干
(3)文件后缀

文件路径标识文件在外部储存设备中的位置,如:

D:\CC  temp(文件路径)  \ file1(文件名主干).dat(文件后缀)

// 表示 file.dat文件存放在D盘中的CC目录下的temp目录下面

11.2 文件指针

FILE *fp  // 其定义一个指针变量fp,该变量用于指向一个文件,存放的是文件缓冲区的首地址。

FILE是文件类型标识符,是C编译系统定义好的一个结构体类型,结构体中含有文件名、文件状态等信息。

11.3 用fopen函数打开数据文件

提示:访问文件的方式一共有12种。由这几个关键字组合而成:

read,write,append(追加),text(文本文件),banary(二进制文件),+表示读和写

fopen函数用来打开一个文件,其调用的方式为:

FILE * fopen(char *filename, char *mode);

函数参数:

参数1: filename:文件名,包括路径,如果不显式含有路径,则表示当前路径。例如,“D:\f1.txt”表示 D 盘根目录下的文件 f1.txt 文件。“f2.doc”表示当前目录下的文件 f2.doc。

参数2:mode:文件打开模式,指出对该文件可进行的操作。常见的打开模式如 “r” 表示只读,“w” 表示只写,“rw” 表示读写,“a” 表示追加写入。更多的打开模式如表 2 所示。

注意:

(1)“文件指针名”必须是被说明为FILE 类型的指针变量; 
(2)“文件名”是被打开文件的文件名;

例如:

FILE *fp; 
fp=("file a","r"); //在当前目录下打开文件file a,只允许进行“读”操作,并使fp指向该文件。 
//定义一个名叫fp文件指针
FILE *fp;
//判断按读方式打开一个名叫test的文件是否失败
if((fp=fopen("test""r") == NULL)//打开操作不成功
{
    printf("The file can not be opened.\n");     
    exit(1);//结束程序的执行
}

文件的操作方式:
模式 | 含义 | 说明

  • | :-: | -:
    “rt” | 只读 | 打开一个文本文件,只允许读数据
    “wt” | 只写 | 打开或建立一个文本文件,只允许写数据
    “at” | 追加 |打开一个文本文件,并在文件末尾写数据
    “rb” |只读 | 打开一个二进制文件,只允许读数据
    “wb” |只写 | 打开或建立一个二进制文件,只允许写数据
    “ab” | 追加 |打开一个二进制文件,并在文件末尾写数据
    “rt+” | 读写 | 打开一个文本文件,允许读和写
    “wt+” | 读写 | 打开或建立一个文本文件,允许读写
    “at+” | 读写 |打开一个文本文件,允许读,或在文件末追加数据
    “rb+” | 读写 | 打开一个二进制文件,允许读和写
    “wb+” | 读写 | 打开或建立一个二进制文件,允许读和写
    “ab+” | 读写 | 打开一个二进制文件,允许读,或在文件末追加数据

返回值:
打开成功,返回该文件对应的 FILE 类型的指针
打开失败,返回 NULL。故需定义 FILE 类型的指针变量,保存该函数的返回值。
可根据该函数的返回值判断文件打开是否成功。

  1. 文件使用方式由r,w,a,t,b,+六个字符拼成,各字符的含义是:
1.  r(read):2.  w(write):3.  a(append): 追加 
4.  t(text): 文本文件,可省略不写 
5.  b(banary): 二进制文件 
6.  +: 读和写 

1) 凡用“r”打开一个文件时,该文件必须已经存在,且只能从该文件读出。
2) 用“w”打开的文件只能向该文件写入。若打开的文件不存在,则以指定的文件名建立该文件,若打开的文件已经存在,则将该文件删去,重建一个新文件。
3) 若要向一个已存在的文件追加新的信息,只能用“a”方式打开文件。但此时该文件必须是存在的,否则将会出错。

11.4 用fclose函数关闭数据函数

1. 函数原型

int fclose(FILE *fp)

2. 功能说明

关闭由fp指出的文件。此时调用操作系统提供的文件关闭功能,关闭由fp->fd指出的文件;释放由fp指出的文件类型结构体变量;返回操作结果,即0EOF

3. 参数说明

 fp:一个已打开文件的文件指针。

4. 返回值
  正常返回:0。
  异常返回:EOF,表示文件在关闭时发生错误。
例如:

 int n=fclose(fp);

该函数把缓冲区内存在的所有数据保存到文件中,关闭文件,释放所有用于该流输入输出缓冲区的内存。函数 fclose()返回 0 表示成功,返回 EOF 表示产生错误。

当程序退出时,所有打开的文件都会自动关闭。尽管如此,还是应该在完成文件处理后,主动关闭文件。否则,一旦遇到非正常的程序终止,就可能会丢失数据。而且,一个程序可以同时打开的文件数量是有限的,数量上限小于等于常量 FOPEN_MAX 的值。

举例:

#include <stdio.h>
void save() {

	FILE *fp;
	if((fp=fopen("stu.txt","a"))==NULL) {
		printf("错误!");
		return;
	}
	//world 输出到文件
	fprintf(fp, "%s", "World");

	fclose(fp);
}
void read() {
	FILE *fp;
	if((fp=fopen("stu.txt","r"))==NULL) {
		printf("错误!");

		return;
	}
	//从文件中输入到 s
//    fscanf(fp, "%s", s);

	int id;
	char name[20];
	char str[20];

	while ( fscanf(fp, "%s",  name) != EOF) {

//        printf("%s\n", name);

	}

	fclose(fp);
}

int main() {

	save();

	read();

	return 0;
}

文件的读写

1)字符读写函数 :fgetc和fputc 
(2)字符串读写函数:fgets和fputs 
(3)数据块读写函数:freed和fwrite 
(4)格式化读写函数:fscanf和fprinf 

使用以上函数都要求包含头文件stdio.h

11.5 函数fgetc和fputc(字符读写)

(1)C语言中提供了从文件中逐个输入字符及向文件中逐个输出字符的顺序读写函数 fgetc 和 fputc 及调整文件读写位置到文件开始处的函数 rewind。这些函数均在标准输入输出头文件 stdio.h 中。

(2)字符读写函数是以字符(字节)为单位的读写函数。每次可从文件读出或向文件写入一个字符。

1. fgetc (读入)

fgetc函数: 从文件中读取一个字符
  1. 函数原型
  int fgetc(FILE *fp);
  2. 功能说明
    从fp所指文件中读取一个字符。
  3. 参数说明
    fp:这是个文件指针,它指出要从中读取字符的文件。
  4. 返回值
    正常返回: 返回读取字符的代码。
    非正常返回:返回EOF。例如,要从"写打开"文件中读取一个字符时,会发生错误而返回一个EOF。

fgetc函数函数调用的形式为

字符变量=fgetc(文件指针)int fgetc (FILE *fp);

例如:

ch=fgetc(fp); // 其意义是从打开的文件fp中读取一个字符并送入ch中。 

读成功,带回所读的字符,失败则返回文件结束标志EOF(-1) 

说明:

1) 在fgetc函数调用中,读取的文件必须是以读或读写方式打开的。 

2) 读取字符的结果也可以不向字符变量赋值, 

例如:

fgetc(fp); // 但是读出的字符不能保存。 

文件操作的简单举例:

#include<stdio.h>
#include<stdlib.h>
int main (void)
{
    char file_name[20]="D:/data—file.txt";
    FILE * fp=fopen (file_name, "w") ; //打开文件
    int c; //c:接收fgetc的返回值,定义为int,而非char M
    if(NULL==fp)
    {
        printf ("Failed tO open the file !\n");
        exit(0);
    }
    printf ("请输入字符,按回车键结束:");
    while ((c=fgetc (stdin)) != '\n') //stdin:指向标准输人设备键盘文件
    {
        fputc (c, stdout); //stdout:指向标准输出设备显示器文件
        fputc(c,fp);
    }
    fputc ('\n', stdout);
    fclose (fp); //关闭文件
    return 0;
}

2. fputc(写)
函数功能:从一个文件流中执行格式化输入,当遇到空格或者换行时结束。注意该函数遇到空格时也结束,这是其与 fgets 的区别,fgets 遇到空格不结束。

1. 函数原型:
int fputc(int ch,FILE *fp)
2. 功能说明
  把ch中的字符写入由fp指出的文件中
  
3. 参数说明
  ch:是一个整型变量,内存要写到文件中的字符(C语言中整型量和字符量可以通用)。
  fp:这是个文件指针,指出要在其中写入字符的文件。
4. 返回值
  正常返回: 要写入字符的代码。
  非正常返回:返回EOF。例如,要往"读打开"文件中写一个字符时,会发生错误而返回一个EOF。
5.fputc函数的调用形式为:

fputc(字符量,文件指量)

例如: fputc(ch',fp);

说明:

被写入的文件可以用写、读写、追加方式打开,写入字符从文件首开始。如需保留原有文件内容,希望写入的字符被写入的文件若不存在,则创建该文件。

#include<stdio.h>
#include<stdlib.h>
#define N 3 //字符串个数
#define MAX_SIZE 30 //字符数组大小,要求每个字符串长度不超过29
int main (void)
{
    char file_name[30]="D:\\file.txt";
    char str[MAX_SIZE];
    FILE *fp;
    int i;
    fp=fopen (file_name, "w+") ; //"w+"模式:先写入后读出
    if(NULL==fp)
    {
        printf ("Failed to open the file !\n");
        exit (0);
    }
    printf ("请输入%d个字符串:\n",N);
    for(i=0;i<N;i++)
    {
        printf ("字符串%d:",i+1);
        fgets (str,MAX_SIZE, stdin) ;//从键盘输入字符串,存入str数组中
        fputs (str, fp) ;//把str中字符串输出到fp所指文件中
    }
    rewind (fp); //把fp所指文件的读写位置调整为文件开始处
    while (fgets(str,MAX_SIZE,fp) !=NULL)
    {
        fputs (str, stdout) ; //把字符串输出到屏幕
    }
    fclose(fp);
    return 0;
}

11.6 文件的随机读写

(1) rewindfseek 函数移动文件读写位置指针。
(2) 使用 ftell 获取当前文件读写位置指针。

1) 函数 fseek 的函数原型为:

int fseek(FI:LE *fp, long offset, int origin);

所在头文件:<stdio.h>

函数功能:把文件读写指针调整到从 origin 基点开始偏移 offset 处,即把文件读写指针移动到 origin+offset 处。

函数参数:
1) origin:文件读写指针移动的基准点(参考点)。基准位置 origin 有三种常量取值:SEEK_SET、SEEK_CUR 和 SEEK_END,取值依次为 0,1,2。

SEEK_SET文件开头,即第一个有效数据的起始位置。
SEEK_CUR当前位置
SEEK_END文件结尾,即最后一个有效数据之后的位置。注意:此处并不能读取到最后一个有效数据,必须前移一个数据块所占的字节数,使该文件流的读写指针到达最后一个有效数据块的起始位置处。

2) offset: 位置偏移量,为 long 型
当 offset 为正整数时,表示从基准 origin 向后移动 offset 个字节的偏移;
若 offset 为负数,表示从基准 origin 向前移动 |offset| 个字节的偏移。

返回值:

成功,返回 0
失败,返回 -1

例如,若 fp 为文件指针
seek (fp,10L,0); fSeek(fp,10L,1); //把读写指针移动到从当前位置向后 10 个字节处。
fseek(fp,-20L,2); // 把读写指针移动到从文件结尾处向前 20 个字节处。

调用 fseek 函数时,第三个实参建议不要使用 0、1、2 等数字,最好使用可读性较强的常量符号形式,使用如下格式取代上面三条语句。
fseek(fp,10L,SEEK_SET);
fseek(fp,10L,SEEK_CUR);
fseek(fp,-20L,SEEK_END);

2) 函数 ftell 的函数原型:
头文件:<stdio.h>

long ftell (FILE *fp); 
函数功能:用于获取当前文件读写指针相对于文件头的偏移字节数。
#include<stdio.h>
#include<stdlib.h>
#define N 3 //动物数
typedef struct {
    char name[10];
    int age;
    char duty[20];
}Animal;
int main (void)
{
    Animal a[N] = {{"兔朱迪",5, "交通警察"}, {"尼克", 8, "协警"},{"闪电",10, "车管所职工"}},t;
    int i;
    FILE *fp=fopen ("Animal_Info.bat", "wb+");
    if(NULL==fp)
    {
        printf("Failed to open the file!\n");
        exit (0);
    }
    fwrite(a,sizeof(Animal),N,fp);
    fprintf (stdout, "%s\t%s\t%s\n", "名字","年龄","职务");
    for(i=1;i<=N;i++)
    {
        fseek(fp,0-i*sizeof(Animal),SEEK_END);
        fread(&t,sizeof(Animal),1,fp);
        fprintf (stdout, "%s\t%d\t%-s\n", t.name,t.age,t.duty);
    }
    fclose(fp);
    return 0;

}
#include<iostream>
using namespace std;
#include<string.h>
#include<fstream>

 int main()
 {
 	char buffer[10]="hello";
 	FILE *fp;
 	fp=fopen("1.txt","W");
	fwrite (buffer,1,strlen(buffer),fp); 
	fread(buffer,1,5,fp);
	buffer[4]='\0';
	cout<<buffer<<endl;
 	fclose(fp);
  } 

12. C++ 面向对象基础(类与对象的认识与理解)



1) 设计一个C++程序设计的基本过程

1.分析问题
2.设计类与对象
3.编辑源程序(文件的扩展名为.cpp)
4.编辑源程序(源程序编辑之后生成的机器指令程序叫做目标程序,其扩展名为.obj)
5.连接程序
6.运行程序(执行.exe文件,得到最终结果)
C++ 是一门面向对象的编程语言,首先要理解类(Class)和对象(Object)这两个概念

2) 类的定义

类是对象的抽象,是一种自定义数据类型,它用于描述一组对象的共同特征和行为。
定义如下:

class 类名  //(1) class 是定义类的关键文字。
{
成员访问限定符;
数据成员;
成员访问限定符;
数据成员;
};         //  (2)  有大括号后面的分号“;”,表示类定义的结束。

C++ 中的类(Class)可以看做C语言中结构体(Struct)的升级版。
结构体是一种构造类型,可以包含若干成员变量,每个成员变量的类型可以不同;可以通过结构体来定义结构体变量,每个变量拥有相同的性质。例如:

#include <stdio.h>

//定义结构体 Student
struct Student{
    //结构体包含的成员变量
    char *name;
    int age;
    float score;
};
//显示结构体的成员变量
void display(struct Student stu){
    printf("%s的年龄是 %d,成绩是 %f\n", stu.name, stu.age, stu.score);
}

int main(){
    struct Student stu1;
    //为结构体的成员变量赋值
    stu1.name = "小明";
    stu1.age = 15;
    stu1.score = 92.5;
    //调用函数
    display(stu1);

    return 0;
}
运行结果: 小明的年龄是 15,成绩是 92.500000

3) 对象创建

1.创建对象

最简单地方法就是给出类型及变量名,格式如下所示:

类型对象列表; //就像定义一个int类型变量一样,int a;a就是int类型的一个对象,按照这样的形式,比如:
Car mycar; 

与 int型变量的定义过程类似,创建类对象就要给其分配空间,存储对象的成员。

2.访问对象成员

创建对象的 目的是访问成员,操作对象的属性及方法。
访问对象成员的语法格式如下:

对象名.成员函数名
访问格式中,“.”为成员运算符,与struct结构体访问成员的方式一样。

例如:

Car mycar;
mycar.disp_welcomemsg();  

访问成员函数的方法与函数调用的形式类似,只是需要适用对象名通过成员运算符访问该函数,例如:

int main()

{
Car mycar;
mycar.disp_welcomemsg();
mycar.set_wheel(4);
cout<<"wheels="<<mycar.get_wheels()<<endl;
system("pasue");
return 0;

}

4) 对象的理解

C++ 中的类也是一种构造类型,但是进行了一些扩展,类的成员不但可以是变量,还可以是函数;
通过类定义出来的变量也有特定的称呼,叫做**“对象”**。例如:

#include <stdio.h>

//通过class关键字类定义类
class Student{
public:
    //类包含的变量
    char *name;
    int age;
    float score;
    //类包含的函数
    void say(){
        printf("%s的年龄是 %d,成绩是 %f\n", name, age, score);
    }
};

int main(){
    //通过类来定义变量,即创建对象
    class Student stu1;  //也可以省略关键字class
    //为类的成员变量赋值
    stu1.name = "小明";
    stu1.age = 15;
    stu1.score = 92.5f;
    //调用类的成员函数
    stu1.say();

    return 0;
}

注意:
(1)结构体和类都可以看做一种由用户自己定义的复杂数据类型,在C语言中可以通过结构体名来定义变量,在 C++ 中可以通过类名来定义变量。不同的是,通过结构体定义出来的变量还是叫变量,而通过类定义出来的变量有了新的名称,叫做对象(Object)。

(2)在第二段代码中,我们先通过 class 关键字定义了一个类 Student,然后又通过 Student 类创建了一个对象 stu1。变量和函数都是类的成员,创建对象后就可以通过点号.来使用它们。

(3)可以将类比喻成图纸,对象比喻成零件,图纸说明了零件的参数(成员变量)及其承担的任务(成员函数);一张图纸可以生产出多个具有相同性质的零件,不同图纸可以生产不同类型的零件。

(4)类只是一张图纸,起到说明的作用,不占用内存空间;对象才是具体的零件,要有地方来存放,才会占用内存空间。

(5)在 C++ 中,通过类名就可以创建对象,即将图纸生产成零件,这个过程叫做类的实例化,因此也称对象是类的一个实例(Instance)

将类的成员变量称为属性(Property),将类的成员函数称为方法(Method)

5) 函数的重载

C++ 允许多个函数拥有相同的名字,只要它们的参数列表不同就可以,这就是函数的重载(Function Overloading)。借助重载,一个函数名可以有多种用途。

void swap1(int *a, int *b);      //交换 int 变量的值
void swap2(float *a, float *b);  //交换 float 变量的值
void swap3(char *a, char *b);    //交换 char 变量的值
void swap4(bool *a, bool *b);    //交换 bool 变量的值

参数列表又叫参数签名,包括参数的类型参数的个数参数的顺序只要有一个不同就叫做参数列表不同。

#include <iostream>
using namespace std;

//交换 int 变量的值
void Swap(int *a, int *b){
    int temp = *a;
    *a = *b;
    *b = temp;
}

//交换 float 变量的值
void Swap(float *a, float *b){
    float temp = *a;
    *a = *b;
    *b = temp;
}

//交换 char 变量的值
void Swap(char *a, char *b){
    char temp = *a;
    *a = *b;
    *b = temp;
}

//交换 bool 变量的值
void Swap(bool *a, bool *b){
    char temp = *a;
    *a = *b;
    *b = temp;
}

int main(){
    //交换 int 变量的值
    int n1 = 100, n2 = 200;
    Swap(&n1, &n2);
    cout<<n1<<", "<<n2<<endl;
   
    //交换 float 变量的值
    float f1 = 12.5, f2 = 56.93;
    Swap(&f1, &f2);
    cout<<f1<<", "<<f2<<endl;
   
    //交换 char 变量的值
    char c1 = 'A', c2 = 'B';
    Swap(&c1, &c2);
    cout<<c1<<", "<<c2<<endl;
   
    //交换 bool 变量的值
    bool b1 = false, b2 = true;
    Swap(&b1, &b2);
    cout<<b1<<", "<<b2<<endl;

    return 0;
}

通过本例可以发现,重载就是在一个作用范围内(同一个类、同一个命名空间等)有多个名称相同但参数不同的函数。重载的结果是让一个函数名拥有了多种用途,使得命名更加方便(在中大型项目中,给变量、函数、类起名字是一件让人苦恼的问题),调用更加灵活。

在使用重载函数时,同名函数的功能应当相同或相近,不要用同一函数名去实现完全不相干的功能,虽然程序也能运行,但可读性不好,使人觉得莫名其妙。

注意:

参数列表不同包括参数的个数不同、类型不同或顺序不同,仅仅参数名称不同是不可以的。函数返回值也不能作为重载的依据。

函数的重载的规则:

函数名称必须相同。
参数列表必须不同(个数不同、类型不同、参数排列顺序不同等)。
函数的返回类型可以相同也可以不相同。
仅仅返回类型不同不足以成为函数的重载。


13. 类与对象的深入


13.1 构造函数

(1) 构造函数的定义
在C++中,有一种特殊的成员函数,它的名字和类名相同,没有返回值,不需要用户显式调用(用户也不能调用),而是在创建对象时自动执行。这种特殊的成员函数就是构造函数(Constructor)。

构造函数首先是一个成员函数,作用是初始化对象的数据成员,特点是他的名字与类名相同,当定义对象时,将自己调用该函数。

定义如下:

类名(参数表)
{
函数体
}

举例1:

class Car{
public:
Car(){

m_strCarName="Rolls-Royce"}
private:
string m_strCarName;
};

举例2:

#include <iostream>
using namespace std;

class Student{
private:
    char *m_name;
    int m_age;
    float m_score;
public:
    //声明构造函数
    Student(char *name, int age, float score);
    //声明普通成员函数
    void show();
};

//定义构造函数
Student::Student(char *name, int age, float score){
    m_name = name;
    m_age = age;
    m_score = score;
}
//定义普通成员函数
void Student::show(){
    cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl;
}

int main(){
    //创建对象时向构造函数传参
    Student stu("小明", 15, 92.5f);
    stu.show();
    //创建对象时向构造函数传参
    Student *pstu = new Student("李华", 16, 96);
    pstu -> show();

    return 0;
}

举例3:

	#include<iostream>
	#include<cmath>
	using namespace std;
	class Point
	{
		public:
		Point(int xx,int yy) { X=xx;Y=yy;}
		Point(Point &p);
		int GetX()  {return X;}
		int GetY()  {return Y;}
		private:
		int X,Y;
	};
	Point::Point(Point &p)
	{
		X=p.X;
		Y=p.Y;
		cout<<"Point 拷贝构造函数被调用"<<endl; 
	 } 
	class Line
	 {
	 	public:
	 		Line(Point a,Point b);
	 		double GetDis() {return dist;}
		private:
		    Point p1,p2;
			double dist;	  
	 };
	 Line::Line(Point a,Point b):p1(a),p2(b)
	 {
	 	cout<<"Line构造函数被调用"<<endl;
		 double x=double(p1.GetX()-p2.GetX());
		 double y=double(p1.GetY()-p2.GetY());
		 dist=sqrt(x*x+y*y); 
	  } 
	  int main()
	  {
	  	Point myp1(1,1),myp2(4,5);
	  	Line myline(myp1,myp2);
	  	cout<<"The distance is:";
		cout<<myline.GetDis()<<endl;
		system("pause");
		return 0;   
	  }

(2)带参构造函数初始化的两种方式

函数体外初始化:
当进行函数体外初始化时,初始化赋值的优先顺序由变量在类内定义中的出现顺序决定的;初始化值列表中初始值的前后位置关系不会影响实际的初始化顺序;

函数体内初始化:
赋值优先级分别为左向右赋值;(默认实参值必须从右向左顺序声明。在默认形参值的右边不能有非默认形参值的参数,因为调用时实参取代形参是从左向右的顺序

(3)构造函数的定义语法规定:

1) 构造函数名与类名相同。
2) 构造函数名前没有返回值类型声明。
3) 构造函数中不能通过return 语句返回一个值。
4) 通常构造函数具有public属性。
5) 构造函数可以通过参数列表进行重载,也就是说,可以定义多个具有不同参数的构造函数,以实现不同数据的初始化。

(4) 构造函数的重载
构造函数是允许重载的。一个类可以有多个重载的构造函数,创建对象时根据传递的实参来判断调用哪一个构造函数。

构造函数的调用是强制性的,一旦在类中定义了构造函数,那么创建对象时就一定要调用,不调用是错误的。如果有多个重载的构造函数,那么创建对象时提供的实参必须和其中的一个构造函数匹配;反过来说,创建对象时只有一个构造函数会被调用。

#include <iostream>
using namespace std;

class Student{
private:
    char *m_name;
    int m_age;
    float m_score;
public:
    Student();
    Student(char *name, int age, float score);
    void setname(char *name);
    void setage(int age);
    void setscore(float score);
    void show();
};

Student::Student(){
    m_name = NULL;
    m_age = 0;
    m_score = 0.0;
}
Student::Student(char *name, int age, float score){
    m_name = name;
    m_age = age;
    m_score = score;
}
void Student::setname(char *name){
    m_name = name;
}
void Student::setage(int age){
    m_age = age;
}
void Student::setscore(float score){
    m_score = score;
}
void Student::show(){
    if(m_name == NULL || m_age <= 0){
        cout<<"成员变量还未初始化"<<endl;
    }else{
        cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl;
    }
}

int main(){
    //调用构造函数 Student(char *, int, float)
    Student stu("小明", 15, 92.5f);
    stu.show();

    //调用构造函数 Student()
    Student *pstu = new Student();
    pstu -> show();
    pstu -> setname("李华");
    pstu -> setage(16);
    pstu -> setscore(96);
    pstu -> show();

    return 0;
}

解释:

构造函数Student(char *, int, float)为各个成员变量赋值,构造函数Student()将各个成员变量的值设置为空,它们是重载关系。根据Student()创建对象时不会赋予成员变量有效值,所以还要调用成员函数 setname()、setage()、setscore() 来给它们重新赋值。

构造函数在实际开发中会大量使用,它往往用来做一些初始化工作,例如对成员变量赋值、预先打开文件等。

(5) 默认构造函数

用户若没有定义构造函数,那么编译器会自动生成一个默认的构造函数,只是这个构造函数的函数体是空的,也没有形参,也不执行任何操作。比如上面的 Student 类,默认生成的构造函数如下:

Student(){}

(1)一个类必须有构造函数,要么用户自己定义,要么编译器自动生成。
(2)一旦用户自己定义了构造函数,不管有几个,也不管形参如何,编译器都不再自动生成。

(3)实际上编译器只有在必要的时候才会生成默认构造函数,而且它的函数体一般不为空。默认构造函数的目的是帮助编译器做初始化工作,而不是帮助程序员。这是C++的内部实现机制,可以理解为“一定有一个空函数体的默认构造函数”来理解。
(4)最后需要注意的一点是,调用没有参数的构造函数也可以省略括号。对于示例2的代码,在栈上创建对象可以写作Student stu()或Student stu,在堆上创建对象可以写作Student *pstu = new Student()或Student *pstu = new Student,它们都会调用构造函数 Student()。

创建对象时都没有写括号,其实是调用了默认的构造函数。

13.2 析构函数

创建对象时系统会自动调用构造函数进行初始化工作,同样,销毁对象时系统也会自动调用一个函数来进行清理工作,例如释放分配的内存、关闭打开的文件等,这个函数就是析构函数

析构函数是类的一种特殊的成员函数,类定义的构造函数在对象之外分配一段堆内存空间,撤销时,由析构函数负责对堆内存释放。

析构函数(Destructor)也是一种特殊的成员函数,没有返回值,不需要程序员显式调用(程序员也没法显式调用),而是在销毁对象时自动执行。构造函数的名字和类名相同,而析构函数的名字是在类名前面加一个~ 符号。

定义形式如下:

类名
{
函数体
~类名():
}

定义析构函数应满足以下要求:

(1)析构函数的名称是在析构函数名称之前添加“~”。
(2)析构函数没有参数。
(3)析构函数中不能通过return返回一个值。
(4)一个类中只能有一个析构函数,不可重载。

举例1:

class Car

{

public:

Car()

{

m_strCarName=“car name”;

m_nSeats=4}

~Car()

{

}

private:

string m_strCarName;

int m_nSeats;

};

举例2:

#include <iostream>
using namespace std;

class VLA{
public:
    VLA(int len);  //构造函数
    ~VLA();  //析构函数
public:
    void input();  //从控制台输入数组元素
    void show();  //显示数组元素
private:
    int *at(int i);  //获取第i个元素的指针
private:
    const int m_len;  //数组长度
    int *m_arr; //数组指针
    int *m_p;  //指向数组第i个元素的指针
};

VLA::VLA(int len): m_len(len){  //使用初始化列表来给 m_len 赋值
    if(len > 0){ m_arr = new int[len];  /*分配内存*/ }
    else{ m_arr = NULL; }
}
VLA::~VLA(){
    delete[] m_arr;  //释放内存
}
void VLA::input(){
    for(int i=0; m_p=at(i); i++){ cin>>*at(i); }
}
void VLA::show(){
    for(int i=0; m_p=at(i); i++){
        if(i == m_len - 1){ cout<<*at(i)<<endl; }
        else{ cout<<*at(i)<<", "; }
    }
}
int * VLA::at(int i){
    if(!m_arr || i<0 || i>=m_len){ return NULL; }
    else{ return m_arr + i; }
}

int main(){
    //创建一个有n个元素的数组(对象)
    int n;
    cout<<"Input array length: ";
    cin>>n;
    VLA *parr = new VLA(n);
    //输入数组元素
    cout<<"Input "<<n<<" numbers: ";
    parr -> input();
    //输出数组元素
    cout<<"Elements: ";
    parr -> show();
    //删除数组(对象)
    delete parr;

    return 0;
}

~VLA()就是 VLA 类的析构函数,它的唯一作用就是在删除对象(第 53 行代码)后释放已经分配的内存。
注意:
(1)函数名是标识符的一种,原则上标识符的命名中不允许出现符号,在析构函数的名字中出现的可以认为是一种特殊情况,目的是为了和构造函数的名字加以对比和区分。

(2)注意:at() 函数只在类的内部使用,所以将它声明为 private 属性;m_len 变量不允许修改,所以用 const 进行了限制,这样就只能使用初始化列表来进行赋值。

(3)C++ 中的 new 和 delete 分别用来分配和释放内存,它们与C语言中 malloc()、free() 最大的一个不同之处在于:用 new 分配内存时会调用构造函数,用 delete 释放内存时会调用析构函数。构造函数和析构函数对于类来说是不可或缺的,所以在C++中我们非常鼓励使用 new 和 delete。

13.3 浅拷贝与深拷贝

拷贝构造函数的定义如下:

class 类名
{

public:
构造函数名称(类名&变量名){
函数体
}

};

在c++程序中,下列情况会自动调用拷贝构造函数:

1)使用一个对象初始化另一个对象。

(2)对象 作为实参传递给函数参数。

(3)函数返回值为类对象,创建临时对象作为返回值。

浅拷贝是指源对象与拷贝对象共用一份实体,仅仅是引用的变量不同(名称不同)。对其中任何一个对象的改动都会影响另外一个对象。

例如:系统默认的拷贝构造函数,会将对象内的成员直接赋值,会造成两个指针存储同一个地方的地址。当析构函数释放了某一个指针所指向的动态资源后,另一个指针就没法用了;

深拷贝是为新对象从堆中再申请出一片空间,使得两个对象相互独立;

13.4 成员访问控制

(1)水平权限:在一个类中,成员的权限控制,就是类中的成员函数能否访问其他成员、类的对象能否访问类中某成员。
(2)垂直权限:在派生类中,对从基类继承来的成员的访问。
(3)内部访问:类中成员函数对其他成员的访问。

(4)外部访问:通过类的对象,访问类的成员函数或者成员变量,有的书里也称之为对象访问。

private,public,protected单纯的作为一个类中的成员(变量和函数)权限设置时:

类的成员函数以及友元函数可以访问类中所有成员,但是在类外通过类的对象,就只能访问该类的共有成员。

注:友元函数包括两种:设为友元的全局函数,设为友元类中的成员函数;这里将友元函数看成内部函数,方便记忆!

总结为下表:

类中属性privateprotectedpublic
内部可见性可见可见可见
外部可见性不可见不可见可见

13.5 静态成员变量与成员函数、内联成员函数

(1)静态成员

静态成员包括静态数据成员与静态成员函数组成。在类内使用static关键字进行定义,在类体外进行初始化;

静态成员的出现则是是为了避免过多的使用全局变量,解决多个对象之间数据共享的安全性问题,。

注意:静态数据成员只能被静态成员函数调用,静态成员函数也只能调用静态数据成员;

使用方式

<类名>::<静态成员函数名>(<参数表>);

<对象名>.<静态成员函数名>(<参数表>)

(2)成员函数

static关键字限定。定义如下:

static 函数返回值类型函数名(形参列表)
{
函数体
}

13.5.3 内联函数

定义形式也是在成员函数的返回值类型前添加inline关键字,具体形式如下:

inline函数返回值类型  函数名(参数表)

{

函数体

}

1、为什么要用内联函数?
在C++中我们通常定义以下函数来求两个整数的最大值:

int max(int a, int b)
{
return a > b ? a : b;
}
为这么一个小的操作定义一个函数的好处有:

① 阅读和理解函数 max 的调用,要比读一条等价的条件表达式并解释它的含义要容易得多

② 如果需要做任何修改,修改函数要比找出并修改每一处等价表达式容易得多

③ 使用函数可以确保统一的行为,每个测试都保证以相同的方式实现

④ 函数可以重用,不必为其他应用程序重写代码

但是写成函数有一个潜在的缺点:调用函数比求解等价表达式要慢得多。在大多数的机器上,调用函数都要做很多工作:调用前要先保存寄存器,并在返回时恢复,复制实参,程序还必须转向一个新位置执行

C++中支持内联函数,其目的是为了提高函数的执行效率,用关键字 inline 放在函数定义(注意是定义而非声明,下文继续讲到)的前面即可将函数指定为内联函数,内联函数通常就是将它在程序中的每个调用点上“内联地”展开,假设我们将 max 定义为内联函数:

inline int max(int a, int b)
{
return a > b ? a : b;
}

则调用: cout << max(a, b) << endl;

在编译时展开为: cout << (a > b ? a : b) << endl; 从而消除了把 max写成函数的额外执行开销。

2、内联函数和宏

1、宏容易出错;
2、宏不可调试;
3、宏无法操作类的私有对象;
4、内联函数可以更加深入的优化;

3. 将内联函数放入头文件
关键字 inline 必须与函数定义体放在一起才能使函数成为内联,仅将 inline 放在函数声明前面不起任何作用。

如下风格的函数 Foo 不能成为内联函数:

inline void Foo(int x, int y);   // inline 仅与函数声明放在一起   
void Foo(int x, int y)
{
    //...
} 
 

而如下风格的函数 Foo 则成为内联函数:

void Foo(int x, int y);   
inline void Foo(int x, int y)   // inline 与函数定义体放在一起

C++ inline函数是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。
一般地,用户可以阅读函数的声明,但是看不到函数的定义。
尽管在大多数教科书中内联函数的声明、定义体前面都加了 inline 关键字,但我认为 inline 不应该出现在函数的声明中。这个细节虽然不会影响函数的功能,但是体现了高质量C++/C 程序设计风格的一个基本原则:声明与定义不可混为一谈,用户没有必要、也不应该知道函数是否需要内联。

定义在类声明之中的成员函数将自动地成为内联函数,例如:

class A
{  
public:
    void Foo(int x, int y) { ... }   // 自动地成为内联函数  
} 

但是编译器是否将它真正内联则要看 Foo函数如何定义

内联函数应该在头文件中定义,这一点不同于其他函数。编译器在调用点内联展开函数的代码时,必须能够找到 inline 函数的定义才能将调用函数替换为函数代码,而对于在头文件中仅有函数声明是不够的。

当然内联函数定义也可以放在源文件中,但此时只有定义的那个源文件可以用它,而且必须为每个源文件拷贝一份定义(即每个源文件里的定义必须是完全相同的),当然即使是放在头文件中,也是对每个定义做一份拷贝,只不过是编译器替你完成这种拷贝罢了。但相比于放在源文件中,放在头文件中既能够确保调用函数是定义是相同的,又能够保证在调用点能够找到函数定义从而完成内联(替换)。

但是你会很奇怪,重复定义那么多次,不会产生链接错误?

我们来看一个例子:


// 文件A.h 代码如下:
复制代码
class A
{
public:
    A(int a, int b) : a(a),b(b){}
    int max();
private:
    int a;
    int b;
};
复制代码
// 文件A.cpp 代码如下:
#include "A.h"
inline int A::max()
{
    return a > b ? a : b;
}
// 文件Main.cpp 代码如下:
复制代码
#include <iostream>
#include "A.h"
using namespace std;
inline int A::max()
{
    return a > b ? a : b;
}

int main()
{
    A a(3, 5);
    cout << a.max() << endl;
    return 0;
}

结果:

一切正常编译,输出结果:5

倘若你在Main.cpp中没有定义max内联函数,那么会出现链接错误:
error LNK2001: unresolved external symbol “public: int __thiscall A::max(void)” (?max@A@@QAEHXZ)main.obj
找不到函数的定义,所以内联函数可以在程序中定义不止一次,只要 inline 函数的定义在某个源文件中只出现一次,而且在所有源文件中,其定义必须是完全相同的就可以。

在头文件中加入或修改 inline 函数时,使用了该头文件的所有源文件都必须重新编译。

4. 慎用内联
“如果所有的函数都是内联函数,还用得着“内联”这个关键字吗?

内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。以下情况不宜使用内联:

(1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。

(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。

类的构造函数和析构函数容易让人误解成使用内联更有效。要当心构造函数和析构函数可能会隐藏一些行为,如“偷偷地”执行了基类或成员对象的构造函数和析构函数。所以不要随便地将构造函数和析构函数的定义体放在类声明中。”
————《高质量程序设计指南——C++/C语言》 林锐

而在Google C++编码规范中则规定得更加明确和详细:

内联函数:
Tip: 只有当函数只有 10 行甚至更少时才将其定义为内联函数.
(1)定义: 当函数被声明为内联函数之后, 编译器会将其内联展开, 而不是按通常的函数调用机制进行调用.

(2)优点: 当函数体比较小的时候, 内联该函数可以令目标代码更加高效. 对于存取函数以及其它函数体比较短, 性能关键的函数, 鼓励使用内联.

(3)缺点: 滥用内联将导致程序变慢. 内联可能使目标代码量或增或减, 这取决于内联函数的大小. 内联非常短小的存取函数通常会减少代码大小, 但内联一个相当大的函数将戏剧性的增加代码大小. 现代处理器由于更好的利用了指令缓存, 小巧的代码往往执行更快。

(4)结论: 一个较为合理的经验准则是, 不要内联超过 10 行的函数. 谨慎对待析构函数, 析构函数往往比其表面看起来要更长, 因为有隐含的成员和基类析构函数被调用!
另一个实用的经验准则: 内联那些包含循环或 switch 语句的函数常常是得不偿失 (除非在大多数情况下, 这些循环或 switch 语句从不被执行).

有些函数即使声明为内联的也不一定会被编译器内联, 这点很重要; 比如虚函数和递归函数就不会被正常内联.
通常, 递归函数不应该声明成内联函数.(递归调用堆栈的展开并不像循环那么简单, 比如递归层数在编译时可能是未知的, 大多数编译器都不支持内联递归函数). 虚函数内联的主要原因则是想把它的函数体放在类定义内, 为了图个方便, 抑或是当作文档描述其行为, 比如精短的存取函数.

-inl.h文件:

Tip: 复杂的内联函数的定义, 应放在后缀名为 -inl.h 的头文件中.
内联函数的定义必须放在头文件中, 编译器才能在调用点内联展开定义. 然而, 实现代码理论上应该放在 .cc 文件中, 我们不希望 .h 文件中有太多实现代码, 除非在可读性和性能上有明显优势.

如果内联函数的定义比较短小, 逻辑比较简单, 实现代码放在 .h 文件里没有任何问题. 比如, 存取函数的实现理所当然都应该放在类定义内. 出于编写者和调用者的方便, 较复杂的内联函数也可以放到 .h 文件中, 如果你觉得这样会使头文件显得笨重, 也可以把它萃取到单独的 -inl.h 中. 这样把实现和类定义分离开来, 当需要时包含对应的 -inl.h 即可。

13.6 this-> 指针

(1)this 到底是什么?

this 实际上是成员函数的一个形参,在调用成员函数时将对象的地址作为实参传递给 this。不过 this 这个形参是隐式的,它并不出现在代码中,而是在编译阶段由编译器默默地将它添加到参数列表中。

this 作为隐式形参,本质上是成员函数的局部变量,所以只能用在成员函数的内部,并且只有在通过对象调用成员函数时才给 this 赋值。

在《C++函数编译原理和成员函数的实现》一节中讲到,成员函数最终被编译成与对象无关的普通函数,除了成员变量,会丢失所有信息,所以编译时要在成员函数中添加一个额外的参数,把当前对象的首地址传入,以此来关联成员函数和成员变量。这个额外的参数,实际上就是 this,它是成员函数和成员变量关联的桥梁。

(2) this指针的用处:

一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。

this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针,编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行。
例如,调用date.SetMonth(9) <===> SetMonth(&date, 9),this帮助完成了这一转换 .

在成员函数内部,我们可以直接使用调用该函数的对象的成员,而无需通过成员访问运算符来做到这一点,因为this所指的正是这个对象。任何对类成员的直接访问都被看成this的隐式使用。

(3) this指针的使用:

一种情况就是,在类的非静态成员函数中返回类对象本身的时候,直接使用 return *this;另外一种情况是当参数与成员变量名相同时,如this->n = n (不能写成n = n)。

this的目的总是指向这个对象,所以this是一个常量指针,我们不允许改变this中保存的地址
举例:

#include <iostream>
using namespace std;

class Student{
public:
    void setname(char *name);
    void setage(int age);
    void setscore(float score);
    void show();
private:
    char *name;
    int age;
    float score;
};

void Student::setname(char *name){
    this->name = name;
}
void Student::setage(int age){
    this->age = age;
}
void Student::setscore(float score){
    this->score = score;
}
void Student::show(){
    cout<<this->name<<"的年龄是"<<this->age<<",成绩是"<<this->score<<endl;
}

int main(){
    Student *pstu = new Student;
    pstu -> setname("李华");
    pstu -> setage(16);
    pstu -> setscore(96.5);
    pstu -> show();

    return 0;
}
本例中成员函数的参数和成员变量重名,只能通过 this 区分。
以成员函数setname(char *name)为例,它的形参是name,和成员变量name重名.
如果写作name = name;这样的语句,就是给形参name赋值,而不是给成员变量name赋值。
而写作this -> name = name;后,=左边的name就是成员变量,右边的name就是形参,一目了然。

注意,this 是一个指针,要用->来访问成员变量或成员函数。

this 虽然用在类的内部,但是只有在对象被创建以后才会给 this 赋值,并且这个赋值的过程是编译器自动完成的,不需要用户干预,用户也不能显式地给 this 赋值。本例中,this 的值和 pstu 的值是相同的。

我们不妨来证明一下,给 Student 类添加一个成员函数printThis(),专门用来输出 this 的值,如下所示:

void Student::printThis(){
    cout<<this<<endl;
}

然后在 main() 函数中创建对象并调用 printThis():

Student *pstu1 = new Student;
pstu1 -> printThis();
cout<<pstu1<<endl;
Student *pstu2 = new Student;
pstu2 -> printThis();
cout<<pstu2<<endl;

运行结果:

0x7b17d8
0x7b17d8
0x7b17f0
0x7b17f0

可以发现,this 确实指向了当前对象,而且对于不同的对象,this 的值也不一样。

几点注意:
this 是 const 指针,它的值是不能被修改的,一切企图修改该指针的操作,如赋值、递增、递减等都是不允许的。
this 只能在成员函数内部使用,用在其他地方没有意义,也是非法的。
只有当对象被创建后 this 才有意义,因此不能在 static 成员函数中使用(后续会讲到 static 成员)。

13.7 友元

关于友元,有两点需要说明:
友元的关系是单向的而不是双向的。如果声明了类 B 是类 A 的友元类,不等于类 A 是类 B 的友元类,类 A 中的成员函数不能访问类 B 中的 private 成员。

友元的关系不能传递。如果类 B 是类 A 的友元类,类 C 是类 B 的友元类,不等于类 C 是类 A 的友元类。

类可以允许其他类或者类外函数访问它的非公有成员,方法是在类内定义一个外部的函数前加上friend关键字令其他类或者函数成为友元,从而达到访问类内非公有成员的目的;

需要注意一下两个概念

1、友元是无法被继承的,也不具备传递性;
2、构造函数、析构函数、拷贝构造函数都是特殊的成员函数,友元则不是成员函数,只是普通的函数被声明为友元;
3、友元并非可以直接访问类的所有成员,而是通过类的对象访问类中的所有成员;
4.友元函数存在的作用就在于更方便的访问类中的所有成员,不需要调用成员函数。

(1)友元函数
友元函数的使用格式为:

friend <返回类型> <函数名>

注意,友元函数不同于类的成员函数,在友元函数中不能直接访问类的成员,必须要借助对象。下面的写法是错误的:

void show(){
    cout<<m_name<<"的年龄是 "<<m_age<<",成绩是 "<<m_score<<endl;
}

成员函数在调用时会隐式地增加 this 指针,指向调用它的对象,从而使用该对象的成员;而 show() 是非成员函数,没有 this 指针,编译器不知道使用哪个对象的成员,要想明确这一点,就必须通过参数传递对象(可以直接传递对象,也可以传递对象指针或对象引用),并在访问成员时指明对象。

使用友元函数需要注意:

1)友元函数能访问类中所有成员函数,一个函数可以是多个类的友元函数,只需在各个类中分别声明为友元即可。
2)C++中不允许将构造函数、析构函数和虚函数声明为友元函数。

(2)友元成员函数
友元成员函数的使用格式为:

    friend 返回类型 类名::函数(形参)

(3)友元类
友元类的使用格式为:

 friend class <类名>

14. 继承


14.1 继承的理解

如果类C是继承于类A的,我们就把类A叫做“基类”(也叫父类),而把类C叫做“派生类”(也叫“子类”)。一个子类继承了它的父类所有可访问的数据成员和函数的一种方式。

① 父类可以派生出多个子类。
② 子类又可以作为父类,再派生出新的派生类。
③ 所有的子孙后代都继承了祖辈的基本特征,同时又有区别和发展。

继承的一般语法为:

class 派生类名:[继承方式] 基类名{
    派生类新增加的成员
};

14.2 访问权限与继承方式

1.公有继承(public):子类继承了父类的公有成员和保护成员,并作为公有成员。而父类的私有成员仍然是私有的,不能被子类访问。

2.私有继承(private):子类继承了父类的公有成员和保护成员,并作为私有成员。而父类的私有成员仍然是私有的,不能被子类访问。

3.保护继承(protected):子类继承了父类的公有成员和保护成员,并作为保护成员。而父类的私有成员仍然是私有的,不能被子类访问。

特别注意:

	(1)  基类的private成员在派生类中不可直接访问。
    (2)  派生类可以进一步限制但不能放松对所继承成员的访问权限。

注意:
(1)类成员的访问权限由高到低依次为 public --> protected --> private;
(2)public 成员可以通过对象来访问,private 成员不能通过对象访问;
(3)protected 成员和 private 成员类似,也不能通过对象访问。但是当存在继承关系时,protected 和 private 就不一样了:基类中的 protected 成员可以在派生类中使用,而基类中的 private 成员不能在派生类中使用

public、protected、private 指定继承方式
不同的继承方式会影响基类成员在派生类中的访问权限

  1. public继承方式
    基类中所有 public 成员在派生类中为 public 属性;
    基类中所有 protected 成员在派生类中为 protected 属性;
    基类中所有 private 成员在派生类中不能使用。

  2. protected继承方式
    基类中的所有 public 成员在派生类中为 protected 属性;
    基类中的所有 protected 成员在派生类中为 protected 属性;
    基类中的所有 private 成员在派生类中不能使用。

  3. private继承方式
    基类中的所有 public 成员在派生类中均为 private 属性;
    基类中的所有 protected 成员在派生类中均为 private 属性;
    基类中的所有 private 成员在派生类中不能使用。

下表汇总了不同继承方式对不同属性的成员的影响结果

继承方式/基类成员public成员protected成员private成员
public继承publicprotected不可见
protected继承protectedprotected不可见
private继承privateprivate不可见

private 和 protected 继承方式会改变基类成员在派生类中的访问权限,导致继承关系复杂,所以实际开发中我们一般使用 public。

#include<iostream>
using namespace std;

//基类People
class People{
public:
    void setname(char *name);
    void setage(int age);
    void sethobby(char *hobby);
    char *gethobby();
protected:
    char *m_name;
    int m_age;
private:
    char *m_hobby;
};
void People::setname(char *name){ m_name = name; }
void People::setage(int age){ m_age = age; }
void People::sethobby(char *hobby){ m_hobby = hobby; }
char *People::gethobby(){ return m_hobby; }

//派生类Student
class Student: public People{
public:
    void setscore(float score);
protected:
    float m_score;
};
void Student::setscore(float score){ m_score = score; }

//派生类Pupil
class Pupil: public Student{
public:
    void setranking(int ranking);
    void display();
private:
    int m_ranking;
};
void Pupil::setranking(int ranking){ m_ranking = ranking; }
void Pupil::display(){
    cout<<m_name<<"的年龄是"<<m_age<<",考试成绩为"<<m_score<<"分,班级排名第"<<m_ranking<<",TA喜欢"<<gethobby()<<"。"<<endl;
}

int main(){
    Pupil pup;
    pup.setname("小明");
    pup.setage(15);
    pup.setscore(92.5f);
    pup.setranking(4);
    pup.sethobby("乒乓球");
    pup.display();

    return 0;
}

改变访问权限
使用 using 关键字可以改变基类成员在派生类中的访问权限,例如将 public 改为 private、将 protected 改为 public。

注意:using 只能改变基类中 public 和 protected 成员的访问权限,不能改变 private 成员的访问权限,因为基类中 private 成员在派生类中是不可见的,根本不能使用,所以基类中的 private 成员在派生类中无论如何都不能访问。

#include<iostream>
using namespace std;

//基类People
class People {
public:
    void show();
protected:
    char *m_name;
    int m_age;
};
void People::show() {
    cout << m_name << "的年龄是" << m_age << endl;
}

//派生类Student
class Student : public People {
public:
    void learning();
public:
    using People::m_name;  //将protected改为public
    using People::m_age;  //将protected改为public
    float m_score;
private:
    using People::show;  //将public改为private
};
void Student::learning() {
    cout << "我是" << m_name << ",今年" << m_age << "岁,这次考了" << m_score << "分!" << endl;
}

int main() {
    Student stu;
    stu.m_name = "小明";
    stu.m_age = 16;
    stu.m_score = 99.5f;
    stu.show();  //compile error
    stu.learning();

    return 0;
}

14.3 基类与派生类

任何基类对象出现的地方都可以用公有派生类对象替代,这被称为类型兼容(向上)原则。这种替代包括以下3种:

(1)派生类对象可以隐含转化为基类对象。

(2)派生类对象可以初始化基类的引用。

(3)派生类指针可以隐含转化为基类指针(基类指针可以指向派生类对象)

注意:类型兼容原则使得公有派生类可以代替基类,这种叫向上兼容

派生类对象当作基类对象使用时,只能使用基类成员。

14.4 单继承与多继承

单继承派生类的声明格式

    class <派生类名> : <继承方式><基类名>{
            <派生类新增加的数据成员>
            <派生类新增加的成员函数>
       };  

注意:继承方式规定了派生类对从基类继承成员的访问权限,继承方式可以是public、protected、private。

如果不显示地给出继承方式,编译系统会默认为private。
公有继承实现的是“is -a”的关系,受保护继承和私有继承则不是。

多继承派生类的定义格式为

class <派生类名> : 继承方式1,基类名1,继承方式2  基类名2,,继承方式n  基类名 n{     
      成员声明;
       };

多继承下的构造函数
多继承形式下的构造函数和单继承形式基本相同,只是要在派生类的构造函数中调用多个基类的构造函数。以上面的 A、B、C、D 类为例,D 类构造函数的写法为:

D(形参列表): A(实参列表), B(实参列表), C(实参列表){
    //其他操作
}

基类构造函数的调用顺序和和它们在派生类构造函数中出现的顺序无关,而是和声明派生类时基类出现的顺序相同。仍然以上面的 A、B、C、D 类为例,即使将 D 类构造函数写作下面的形式:

D(形参列表): B(实参列表), C(实参列表), A(实参列表){
    //其他操作
}

先调用 A 类的构造函数,再调用 B 类构造函数,最后调用 C 类构造函数。

#include <iostream>
using namespace std;

//基类
class BaseA{
public:
    BaseA(int a, int b);
    ~BaseA();
protected:
    int m_a;
    int m_b;
};
BaseA::BaseA(int a, int b): m_a(a), m_b(b){
    cout<<"BaseA constructor"<<endl;
}
BaseA::~BaseA(){
    cout<<"BaseA destructor"<<endl;
}

//基类
class BaseB{
public:
    BaseB(int c, int d);
    ~BaseB();
protected:
    int m_c;
    int m_d;
};
BaseB::BaseB(int c, int d): m_c(c), m_d(d){
    cout<<"BaseB constructor"<<endl;
}
BaseB::~BaseB(){
    cout<<"BaseB destructor"<<endl;
}

//派生类
class Derived: public BaseA, public BaseB{
public:
    Derived(int a, int b, int c, int d, int e);
    ~Derived();
public:
    void show();
private:
    int m_e;
};
Derived::Derived(int a, int b, int c, int d, int e): BaseA(a, b), BaseB(c, d), m_e(e){
    cout<<"Derived constructor"<<endl;
}
Derived::~Derived(){
    cout<<"Derived destructor"<<endl;
}
void Derived::show(){
    cout<<m_a<<", "<<m_b<<", "<<m_c<<", "<<m_d<<", "<<m_e<<endl;
}

int main(){
    Derived obj(1, 2, 3, 4, 5);
    obj.show();
    return 0;
}

运行结果:
BaseA constructor
BaseB constructor
Derived constructor
1, 2, 3, 4, 5
Derived destructor
BaseB destructor
BaseA destructor

14.5 struct和class

在c++中并没有本质区别,最主要的一个差别就是默认的继承和访问方式不同:

  (1) 默认继承方式不同,struct默认是public,class默认的是private。
  (2) 默认成员的访问权限不同:struct默认是public,class默认是private。

14.6 派生类的构造函数和析构函数

(1) 构造函数

派生类构造函数的一般语法形式为:

派生类名 : : 派生类名(参数表) :基类名(基类初始化参数表)
{
//  派生类构造函数的其他初始化操作。
}

注意:

  1. 派生类如果没有显示定义构造函数,这时派生类默认的构造函数会自动调用基类的无参默认构造函数。
  2. 如果不需要调用基类的带参数的构造函数,则初始化列表对基类构造函数的调用可以省略。
    3)如果对派生类初始化时,需要调用基类的带参数的构造函数时,派生类就必须声明构造函数,提供一个将参数传递给基类构造函数的途径,保证在基类进行初始化时能够获得必要的数据。
#include<iostream>
using namespace std;

//基类People
class People{
protected:
    char *m_name;
    int m_age;
public:
    People(char*, int);
};
People::People(char *name, int age): m_name(name), m_age(age){}

//派生类Student
class Student: public People{
private:
    float m_score;
public:
    Student(char *name, int age, float score);
    void display();
};
//People(name, age)就是调用基类的构造函数
Student::Student(char *name, int age, float score): People(name, age), m_score(score){ }
void Student::display(){
    cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<"。"<<endl;
}

int main(){
    Student stu("小明", 16, 90.5);
    stu.display();

    return 0;
}

(2)拷贝构造函数

如果一个构造函数的第一个参数是自身类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。

不论拷贝构造函数时显式还是隐式,派生类的拷贝函数都需要调用基类的拷贝构造函数以完成对从基类继承的成员的初始化,传递参数的参数是派生类对象的一个引用。

(3)析构函数

析构函数没有返回值,也不接受参数:

  class A{
  public:
      ~A();   //析构函数
  };
(1)由于析构函数不接受参数,因此它不能被重载。
(2)一个类,只允许有一个析构函数。
(3)在一个构造函数中,成员的初始化时在函数体执行之前完成的,且按照它们在类中出现的顺序进行初始化。在一个析构函数中,首先执行函数体,然后销毁成员。成员按初始化顺序的逆序进行销毁。

无论何时一个对象被销毁,就会自动调用其析构函数:

1.变量在离开其作用域时被销毁
2.当一个对象被销毁时,其成员被销毁
3.容器(无论是标准容器还是数组)被销毁时,其元素被销毁

#include <iostream>
using namespace std;

class A{
public:
    A(){cout<<"A constructor"<<endl;}
    ~A(){cout<<"A destructor"<<endl;}
};

class B: public A{
public:
    B(){cout<<"B constructor"<<endl;}
    ~B(){cout<<"B destructor"<<endl;}
};

class C: public B{
public:
    C(){cout<<"C constructor"<<endl;}
    ~C(){cout<<"C destructor"<<endl;}
};

int main(){
    C test;
    return 0;
}
运行结果:
A constructor
B constructor
C constructor
C destructor
B destructor
A destructor
和构造函数类似,析构函数也不能被继承。与构造函数不同的是,在派生类的析构函数中不用显式地调用基类的析构函数,因为每个类只有一个析构函数,编译器知道如何选择,无需程序员干涉。

另外析构函数的执行顺序和构造函数的执行顺序也刚好相反

(1)创建派生类对象时,构造函数的执行顺序和继承顺序相同,即先执行基类构造函数,再执行派生类构造函数。
(2)而销毁派生类对象时,析构函数的执行顺序和继承顺序相反,即先执行派生类析构函数,再执行基类析构函数。

14.7 区分函数重载和隐藏

重载:同一个函数名,不同形参。调用时根据传入的实参判断到底是哪个函数。

需要注意的是,重载函数必须有不同的参数列表(可以是参数的个数,次序或类型不同),而不能仅仅依赖于函数的不同返回类型来重载函数;

(1)重载发生在作用域相同,函数名相同,但参数个数、类型不同的场合。
(2)隐藏发生在两个或多个嵌套的作用域,函数名字相同,参数的个数、类型可以相同,也可以在不同的场合。

14.8 虚继承

为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。

虚继承的语法格式如下:

  class  派生类名:virtual  继承方式  基类名{
 	// 派生类新增的数据成员和成员函数
};

举例:

class  Lionvirtual public Animal
{

};
class Tigervirtual public Animal
{

};
class Liger:public lion,public Tiger
{

};
//间接基类A
class A{
protected:
    int m_a;
};

//直接基类B
class B: virtual public A{  //虚继承
protected:
    int m_b;
};

//直接基类C
class C: virtual public A{  //虚继承
protected:
    int m_c;
};

//派生类D
class D: public B, public C{
public:
    void seta(int a){ m_a = a; }  //正确
    void setb(int b){ m_b = b; }  //正确
    void setc(int c){ m_c = c; }  //正确
    void setd(int d){ m_d = d; }  //正确
private:
    int m_d;
};

int main(){
    D d;
    return 0;
}

虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。
在这里插入图片描述

15. 多态

15.1 什么是多态

多态(polymorphism)”指的是同一名字的事物可以完成不同的功能。多态可以分为编译时的多态和运行时的多态。前者主要是指函数的重载(包括运算符的重载)、对重载函数的调用,在编译时就能根据实参确定应该调用哪个函数,因此叫编译时的多态;而后者则和继承、虚函数等概念有关

多态是面向对象程序设计的重要特征之一,它与抽象、封装、继承共同构成了面向对象程序设计的四大特征。

一般通过继承和虚函数来实现。

15.2 虚函数

虚函数成员声明的语法如下:

virtual 函数类型 函数名 (形参表);

注意:

(1)如果一个函数在基类中被声明为virtual,那么在所有的派生类中它都是virtual,也就是说虚函数具有继承性。为了程序的可读性,在派生类再次用virtual声明。
(2)virtual只对函数声明有意义,在函数定义时,不能再次使用。
(3)虚函数必须是non-static成员函数。

举例:

#include <iostream>
using namespace std;

//基类People
class People{
public:
    People(char *name, int age);
    virtual void display();  //声明为虚函数
protected:
    char *m_name;
    int m_age;
};
People::People(char *name, int age): m_name(name), m_age(age){}
void People::display(){
    cout<<m_name<<"今年"<<m_age<<"岁了,是个无业游民。"<<endl;
}

//派生类Teacher
class Teacher: public People{
public:
    Teacher(char *name, int age, int salary);
    virtual void display();  //声明为虚函数
private:
    int m_salary;
};
Teacher::Teacher(char *name, int age, int salary): People(name, age), m_salary(salary){}
void Teacher::display(){
    cout<<m_name<<"今年"<<m_age<<"岁了,是一名教师,每月有"<<m_salary<<"元的收入。"<<endl;
}

int main(){
    People *p = new People("王志刚", 23);
    p -> display();

    p = new Teacher("赵宏佳", 45, 8200);
    p -> display();

    return 0;
}

15.3 纯虚函数和抽象类

纯虚函数是一个在基类中没有定义具体操作的虚函数,要求必须在派生类中根据实际需要给出各自的定义。

纯虚类函数的声明语法为:

virtual 函数类型 函数名(参数表)=0;

(1)带有纯虚函数的类称为抽象类
(2) 纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上=0,表明此函数为纯虚函数。

最后的=0并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是纯虚函数”。

(3)包含纯虚函数的类称为抽象类(Abstract Class)。说它抽象,是因为它无法实例化,也就是无法创建对象。原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。

抽象类通常是作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。

注意:

抽象类不能实例化。
派生类必须完成定义纯虚函数的职责。

#include <iostream>
using namespace std;

//线
class Line{
public:
    Line(float len);
    virtual float area() = 0;
    virtual float volume() = 0;
protected:
    float m_len;
};
Line::Line(float len): m_len(len){ }

//矩形
class Rec: public Line{
public:
    Rec(float len, float width);
    float area();
protected:
    float m_width;
};
Rec::Rec(float len, float width): Line(len), m_width(width){ }
float Rec::area(){ return m_len * m_width; }

//长方体
class Cuboid: public Rec{
public:
    Cuboid(float len, float width, float height);
    float area();
    float volume();
protected:
    float m_height;
};
Cuboid::Cuboid(float len, float width, float height): Rec(len, width), m_height(height){ }
float Cuboid::area(){ return 2 * ( m_len*m_width + m_len*m_height + m_width*m_height); }
float Cuboid::volume(){ return m_len * m_width * m_height; }

//正方体
class Cube: public Cuboid{
public:
    Cube(float len);
    float area();
    float volume();
};
Cube::Cube(float len): Cuboid(len, len, len){ }
float Cube::area(){ return 6 * m_len * m_len; }
float Cube::volume(){ return m_len * m_len * m_len; }

int main(){
    Line *p = new Cuboid(10, 20, 30);
    cout<<"The area of Cuboid is "<<p->area()<<endl;
    cout<<"The volume of Cuboid is "<<p->volume()<<endl;
  
    p = new Cube(15);
    cout<<"The area of Cube is "<<p->area()<<endl;
    cout<<"The volume of Cube is "<<p->volume()<<endl;

    return 0;
}

运行结果:
The area of Cuboid is 2200
The volume of Cuboid is 6000
The area of Cube is 1350
The volume of Cube is 3375

抽象基类除了约束派生类的功能,还可以实现多态。
请注意第 51 行代码,指针 p 的类型是 Line,但是它却可以访问派生类中的 area() 和 volume() 函数,正是由于在 Line 类中将这两个函数定义为纯虚函数;如果不这样做,51 行后面的代码都是错误的。这或许才是C++提供纯虚函数的主要目的。

关于纯虚函数的几点说明

  1. 一个纯虚函数就可以使类成为抽象基类,但是抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数(虚函数或普通函数)和成员变量。

  2. 只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数。如下例所示:

//顶层函数不能被声明为纯虚函数
void fun() = 0;   //compile error

class base{
public :
    //普通成员函数不能被声明为纯虚函数
    void display() = 0;  //compile error
};

16. 封装

封装,即隐藏对象的属性和实现细节,仅对外公开接口,控制在程序中属性的读和修改的访问级别;将抽象得到的数据和行为(或功能)相结合,形成一个有机的整体,也就是将数据与操作数据的源代码进行有机的结合,形成“类”,其中数据和函数都是类的成员。

一般会用到 set接口和get出口

#include <iostream>
#include <string>
using namespace std;

/**
  * 定义类:Student
  * 数据成员:m_strName
  * 数据成员的封装函数:setName()、getName()
  */
class Student
{
public:
    // 定义数据成员封装函数setName()
    void setName(string name) {
        m_strName = name;
    }
    
    
    // 定义数据成员封装函数getName()
    string getName() {
        return m_strName;
    }
    
    
//定义Student类私有数据成员m_strName
private:
    string m_strName;

};

int main()
{
    // 使用new关键字,实例化对象
	Student *str = new Student;
    // 设置对象的数据成员
	str->setName("cpp");
    // 使用cout打印对象str的数据成员
    cout << str->getName() << endl;
    // 将对象str的内存释放,并将其置空
	delete str;
	str = NULL;
	return 0;
}

17. 文件和文件流

17.1 文件的概念

对于用户来说,常用到的文件有两大类:
程序文件和数据文件。根据文件中数据的组织方式,则可以将文件分为 ASCII 文件和二进制文件。

(1)ASCII 文件又称字符文件或者文本文件,它的每一个字节放一个 ASCII 代码,代表一个字符。
(2)二进制文件又称内部格式文件或字节文件,是把内存中的数据按其在内存中的存储形式原样输出到磁盘上存放。

数字 64 在内存中表示为 0100 0000,若将其保存为 ASCII 文件,则要分别存放十位 6 和个位 4 的 ASCII 码,为 0011 0110 0011 0100,占用两个字节;若将其保存为二进制文件,则按内存中形式直接输出,为 0100 0000,占用一个字节。

ASCII 文件中数据与字符一一对应,一个字节代表一个字符,可以直接在屏幕上显示或打印出来,这种方式使用方便,比较直观,便于阅读,但一般占用存储空间较大,而且输出时要将二进制转化为 ASCII 码比较花费时间。

(2)二进制文件,输出时不需要进行转化,直接将内存中的形式输出到文件中,占用存储空间较小,但一个字节并不对应一个文件,不能直观显示文件中的内容。

17.2 文件流对象的创建

文件流是以外存文件未输入输出对象的数据流。输出文件流是从内存流向外存文件的数据,输入文件流是从外存文件流向内存的数据。每一个文件流都有一个内存缓冲区与之对应。

假设文件流对象infile和outfile,想要操作磁盘文件“in”和“out”,就可编写如下代码完成用文件流对象打开文件:

ifstream  infile(“in”);//打开当前目录下的“in”文件
ofstream outfile(“out”);//打开当前目录的“out”文件

以上代码在定义文件流对象的同时打开文件,其中infile是输入文件流对象,用于从文件读取数据,outfile是输出文件流对象,用于向文件写入数据。

(1)在ios类中定义的文件打开方式

ios::app打开一个文件使新的内容始终添加在文件的末尾
ios::ate打开一个文件使新的内容添加在文件尾,但下次添加时,写在当前位置处
ios::inv以输入(读)方式打开文件
ios::out以输出(写)方式打开文件
ios::trunc若文件存在,则清除文件所有内容,若文件不存在,则创建新文件
ios::binary以二进制方式打开文件,缺省时以文本方式打开文件
ios::nocreate打开一个已有文件,若该文件不存在,则打开失败
ios::noreplace若打开的文件已经存在,则打开失败
特别说明: (1)out 、turnc和app方式只能用于与ofstream或fstream对相关联的文件。
(2)in方式只能用于与ifstream或fstream对象关联的文件。
(3)所有文件都可以用ate或binary方式打开。

(2)ofstream、ifstream、fstream

C++ 中有三个用于文件操作的文件类:

ifstream 类,它是从 istream 类派生来的,用于支持从磁盘文件的输入。
ofstream 类,它是从 ostream 类派生来的,用于支持向磁盘文件的输出。
fstream 类,它是从 iostream 类派生来的,用于支持对磁盘文件的输入输出。
要以磁盘文件为对象进行输入输出,必须定义一个文件流类的对象,通过文件流对象将数据从内存输出到磁盘文件,或者将磁盘文件输入到内存。
ofstream 该数据类型表示输出文件流,用于创建文件并向文件写入信息。
ifstream 该数据类型表示输入文件流,用于从文件读取信息。
fstream 该数据类型通常表示文件流,且同时具有 ofstream 和 ifstream 两种功能,这意味着它可以创建文件,向文件写入信息,从文件读取信息。

三种常用文件流对象与打开方式的关系描述如下:
(1)ifstream文件流对象默认的打开方式为 in,该方式允许进行文件的读取操作。

(2)ofstream 文件流对象默认的打开方式为out,该方式允许进行文件的存入操作。

需要注意的是以out方式打开文件,文件的内容会被清空。

(3)fstream文件流对象默认以in和out方式打开文件,该方式允许对一个文件同时进行读写操作。

(4)检验文件是否打开成功

if(!infile)
{
cerr<<"'error:unable to open input file: "<<infile<<endl;
return -1}

17.3 文件流对象的关闭

infile.close();
outfile.close();

17.4 C++文件流的顺序读写

ifstream infile;// 定义输入文件类对象
infile.open(“myfile1.txt”);//利用函数打开某一文件
ifstream outfile;//定义输出文件类对象
infile.open(“myfile2.txt”);//利用函数打开某一文件

(1)get()与put()函数

get()和put()函数为单个字符的读取和写入函数。get()函数可以从文件中读取一个字符,put()可以向文件写入一个字符。
如:

infile.get(ch);
outfile.put(ch);

(2)getline(char*int)

getline是从文件中读取一行文本,最多N个字符,传入到字符串数组中。

如: infile.getline(ch,30)// 从文件中最多读取30个字符至字符串ch中

17.5 C++文件流的随机读写

1.seekg()和seekp()

seekg 和 seekp 的参数通常是一个长整型。第二个参数可以用于指定查找方向。查找方向可以是 ios::beg(默认的,从流的开头开始定位),也可以是 ios::cur(从流的当前位置开始定位),也可以是 ios::end(从流的末尾开始定位)

seekg(): 是对输入文件定位
seekp() :是对输出文件定位
它们各有2个参数:第一个参数代码偏移量,第二个参数是基地址。

其中第一个参数可以为正负数值,正的表示向后偏移,负的表示向前偏移。

第二个参数可以是以下值:

 (1) 0 或者ios::beg:表示输入文件流的开始位置。
 (2) 1或ios::cur: 表示输入文件流的当前位置。
 (3) 2或者ios::end :表示输入文件结束位置。

stream 和 ostream 都提供了用于重新定位文件位置指针的成员函数。这些成员函数包括关于 istream 的 seekg(“seek get”)和关于 ostream 的 seekp(“seek put”)。

文件位置指针是一个整数值,指定了从文件的起始位置到指针所在位置的字节数。下面是关于定位 “get” 文件位置指针的实例:

// 定位到 fileObject 的第 n 个字节(假设是 ios::beg)
fileObject.seekg( n );
 
// 把文件的读指针从 fileObject 当前位置向后移 n 个字节
fileObject.seekg( n, ios::cur );
 
// 把文件的读指针从 fileObject 末尾往回移 n 个字节
fileObject.seekg( n, ios::end );
 
// 定位到 fileObject 的末尾
fileObject.seekg( 0, ios::end );

2.tellg()和tellp()

tellg():返回输入文件的当前位置
tellp():返回输出文件的当前位置
这两个函数都不需要参数,它们的返回值就是当前文件的读写位置。

3.efo()

用判断当前文件的读写位置是否位于结尾,返回值true(位于结尾)或false(没有在结尾)。

17.6 C++文件流的使用

(1)读取 & 写入实例

下面的 C++ 程序以读写模式打开一个文件。在向文件 afile.dat 写入用户输入的信息之后,程序从文件读取信息,并将其输出到屏幕上:

#include <fstream>
#include <iostream>
using namespace std;
 
int main ()
{
    
   char data[100];
 
   // 以写模式打开文件
   ofstream outfile;
   outfile.open("afile.dat");
 
   cout << "Writing to the file" << endl;
   cout << "Enter your name: "; 
   cin.getline(data, 100);
 
   // 向文件写入用户输入的数据
   outfile << data << endl;
 
   cout << "Enter your age: "; 
   cin >> data;
   cin.ignore();
   
   // 再次向文件写入用户输入的数据
   outfile << data << endl;
 
   // 关闭打开的文件
   outfile.close();
 
   // 以读模式打开文件
   ifstream infile; 
   infile.open("afile.dat"); 
 
   cout << "Reading from the file" << endl; 
   infile >> data; 
 
   // 在屏幕上写入数据
   cout << data << endl;
   
   // 再次从文件读取数据,并显示它
   infile >> data; 
   cout << data << endl; 
 
   // 关闭打开的文件
   infile.close();
 
   return 0;
}

当上面的代码被编译和执行时,它会产生下列输入和输出:

$./a.out
Writing to the file
Enter your name: Zara
Enter your age: 9
Reading from the file
Zara
9
上面的实例中使用了 cin 对象的附加函数,比如 getline()函数从外部读取一行,ignore() 函数会忽略掉之前读语句留下的多余字符。

(2) 对 ASCII 文件的操作

#include <iostream>
#include <fstream>

using namespace std;

int main()
{
    ofstream outfile("a.txt", ios::out);

    if (!outfile)
    {
        cerr << "Failed to open the file!";
        return 1;
    }

    // 写入数字 1-5 到文件中
    for (int i = 1; i < 6; i++)
    {
        outfile << i << '\n';
    }

    outfile.close();

    ifstream infile("a.txt", ios::in);

    if (!infile)
    {
        cerr << "Failed to open the file!";
        return 1;
    }

    char data;  // 从文件中读出数字 1-5 
    for (int i = 1; i < 6; i++)
    {
        infile >> data;
        cout << data << '\n';
    }

    infile.close();

    return 0;
}

(3) 对二进制文件的操作

二进制文件的操作需要在打开文件的时候指定打开方式为 ios::binary,并且还可以指定为既能输入又能输出的文件,我们通过成员函数 read 和 write 来读写二进制文件。

istream& read (char* s, streamsize n);
ostream& write (const char* s, streamsize n);

#include <iostream>
#include <fstream>

using namespace std;

int main()
{
    ofstream outfile("a.txt", ios::binary);

    if (!outfile)
    {
        cerr << "Failed to open the file!";
        return 1;
    }

    char a[] = {'h', 'e', 'l', 'l', 'o', ','};
    char b[] = {'s', 'e', 'n', 'i', 'u', 's', 'e', 'n', '!'};

    outfile.write(a, 6); // 将以 a 为首地址的 6 个字符写入文件
    outfile.write(b, 9);
    outfile.close();

    ifstream infile("a.txt", ios::binary);

    if (!infile)
    {
        cerr << "Failed to open the file!";
        return 1;
    }

    char data[6];
    infile.read(data, 6);  // 从文件中读出 6 个字符到以 data 为首地址的字符数组中
    for (int i = 0; i < 6; i++)
    {
        cout << data[i];
    }

    char datb[6];
    infile.read(datb, 9);
    for (int i = 0; i < 9; i++)
    {
        cout << datb[i];
    }

    infile.close();

    return 0;
}

(4) 追加文件
前面提到过当我们用ofstream打开文件进行写入时,是覆盖写入,如果想要追加写入,可以再open里面加入参数。
用:

ofstream ofile;
ofile.open(name.c_str(),ios::out | ios::app); //之前没有后面这个参数,但是默认的是ios::out,即输出模式,这里因为为追加写入,所以要加入新的参数:ios::app
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>

using namespace std;
const string name = "test_file13.txt"; //the file name

int main()
{
    //show initial content
    char ch;
    ifstream ifile;
    ifile.open(name.c_str());

    if(ifile.is_open())
    {
        cout << '\n'<<"here are the current contents of the" << name <<":"<<endl;
        while(ifile.get(ch))
        {
            cout << ch;
        }
        ifile.close();
    }

    //add new content
    ofstream ofile;
    ofile.open(name.c_str(),ios::out|ios::app);
    if(!ofile.is_open())
    {
        cout << "can not open" << name <<endl;

    }

    if(ofile.is_open())
    {
        char a;
        cout <<'\n'<< "please enter your content:";
        a = cin.get();  //注意这里:这里就是将用户的输入写入文件,用的方法就是一个一个写入,直到遇到回车。
        while(a != '\n')
        {
            ofile << a;
            a = cin.get();
        }
        ofile << endl;
        ofile.close();
    }


    //show revised file

    ifile.open(name.c_str());

    if(ifile.is_open())
    {
        cout <<'\n'<< "here are the current contents of the" << name <<":"<<endl;
        while(ifile.get(ch))
        {
            cout << ch;
        }
        ifile.close();
        cout << endl;
    }
    return 0;
}

18. 树与二叉树

在这里插入图片描述

18.1 树的概念

树状图是一种数据结构,它是由n(n>=1)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:
每个节点有零个或多个子节点;没有父节点的节点称为根节点;每一个非根节点有且只有一个父节点;除了根节点外,每个子节点可以分为多个不相交的子树;
在这里插入图片描述

树(tree)是包含n(n>0)个结点的有穷集,其中:
(1)每个元素称为结点(node);
(2)有一个特定的结点被称为根结点或树根(root)。
(3)除根结点之外的其余数据元素被分为m(m≥0)个互不相交的集合T1,T2,……Tm-1,其中每一个集合Ti(1<=i<=m)本身也是一棵树,被称作原树的子树(subtree)。

树的定义还可形成化的描述为二元组的形式:

T=(D,R)

其中D为树T中结点的集合,R为树中结点之间关系的集合。

树属于半线性结构

18.2 树的相关术语

(1)结点的度与树的度:结点所拥有的子树的个数称为该结点的度,树中个结点度的最大值为该树的度。
(2)叶子结点:度为0的结点称为叶子结点,或称为终端结点。
(3)分支结点:度不为0的结点称为分支结点。一棵树的结点除叶结点外,其余的都是分支节点。
(4)孩子结点、父结点和兄弟结点:一个结点的子树的根结点称为这个结点的孩子结点。这个结点成为它的孩子结点的父结点。
具有同一个父结点的孩子互称为兄弟。
(5)树的深度:树中所有节点的最大层数称为树的深度。
(6)有序数和无序树:如果一棵树中结点的各子树从左到右是有序的,即若交换了某结点各子树的相对位置,则构成不同的树,称为这棵树为有序树,反之,则称为无序树。

节点的度:一个节点含有的子树的个数称为该节点的度;
叶节点或终端节点:度为0的节点称为叶节点;
非终端节点或分支节点:度不为0的节点;
双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;
孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;
兄弟节点:具有相同父节点的节点互称为兄弟节点;
树的度:一棵树中,最大的节点的度称为树的度;
节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
树的高度或深度:树中节点的最大层次;
堂兄弟节点:双亲在同一层的节点互为堂兄弟;
节点的祖先:从根到该节点所经分支上的所有节点;
子孙:以某节点为根的子树中任一节点都称为该节点的子孙。
森林:由m(m>=0)棵互不相交的树的集合称为森林;

18.3 二叉树

18.3.1 二叉树的定义

二叉树是一种特殊的树,二叉树中的结点至多只能有两个子结点。
就是度不超过2的树(节点最多有两个叉)

二叉树的定义:

(1)由有限个结点所构成的集合,此集合可以是空的。
(2)二叉树的根结点下可分为两个子树,称为左子树和右子树,左子树和右子树亦分别是二叉树。
在这里插入图片描述

注意:

二叉树是有序的,即若将左、右子树颠倒,就成为另一棵不同的二叉树。即使树中结点只有一棵二叉树,也要去区分它是左或右子树。

二叉树是树的特例,二叉树的度必为0、1、或2,而一般的树的度可为任意非负整数。

18.3.2 二叉树的概念与形态

(1)满二叉树:如果一棵二叉树的结点要么是叶子结点,要么它有两个子结点,这样的树就是满二叉树。(一棵满二叉树的每一个结点要么是叶子结点,要么它有两个子结点,但是反过来不成立,因为完全二叉树也满足这个要求,但不是满二叉树)
一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。

(2)完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。
叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树
满二叉树一定是完全二叉树,但是完全二叉树不一定是满二叉树
在这里插入图片描述

18.4 二叉树的遍历

二叉树的遍历方式有三种,前序遍历,中序遍历,后序遍历。
在这里插入图片描述

它的前序遍历顺序为:ABDGHCEIF(规则是先是根结点,再前序遍历左子树,再前序遍历右子树)
它的中序遍历顺序为:GDHBAEICF(规则是先中序遍历左子树,再是根结点,再是中序遍历右子树)
它的后序遍历顺序为:GHDBIEFCA(规则是先后序遍历左子树,再是后序遍历右子树,再是根结点)
在这里插入图片描述

18.5 树的性质

对于度为k的树:
1、节点数=度数+1
2、第i层最多节点数:k(i-1),i≥1
3、高为i的k叉树节点数最多:(ki-1)/(k-1),i≥1
4、n个节点的k叉树深度最小为:ceil( logk( n(k-1)+1 ) )

18.6 二叉树的性质

性质1. 非空二叉树第 i 层上至多有 2i 个结点(i ≥ 0)
性质2. 高度为 k 的二叉树至多有 2k-1 个结点(k ≥ 0)
性质3. 对任何非空二叉树 T,若其叶结点个数为 n0,度数为 2 的结点
个数为 n2,则n0 = n2 + 1
性质4. n 个结点的完全二叉树的高度 k = ⎡log2(n+1)⎤
性质5. 满二叉树里的叶结点比分支结点多一个

18.7 二叉树的链式存储实例

18.7.1 二叉树的创建

 node* _create_tree(T*a, size_t n, const T& invalid, size_t& index)
    {
        node* root = NULL;
        if (a[index] != invalid)
        {
            root = new node(a[index]);
            root->_left = _create_tree(a, n, invalid, ++index);
            root->_right = _create_tree(a, n, invalid, ++index);
        }
        return root;
    }

18.7.2 二叉树的创建

int f;
struct node *p,*q;
p=tree;
f=0;
q=( struct node *)malloc(sizeof (struct node));
while((!f)&&(p!=NULL)) {
	if(p->data==x)
		f=1;
	else if(x<p->data)
		p=p->llink;
	else
		p=p->rlink;
}
if(f)
	q=p;
else
	q->data=NULL;
return(q);

18.7.3 二叉树的插入

struct node *p,*q;
if(tree==NULL) {
	p= (struct node *)malloc(sizeof(struct node));
	p->data=x;
	p->rlink=NULL;
	p->llink=NULL;
	tree=p;
} else {
	p=tree;
	while(p!=NULL) {
		q=p;
		if (x<p->data)
			p=p->llink;
		else
			p=p->rlink;
	}
	p=(struct node *)malloc(sizeof(struct node));
	p->data=x;
	p->rlink=NULL;
	p->llink=NULL;
	if (x<q->data)
		q->llink=p;
	else
		q->rlink=p;
}
return tree;

18.7.4 二叉树的删除

struct node *q,*s;
int boo;
boo=0;
if((p->llink==NULL)||(p->rlink==NULL)) {
	if(p->llink==NULL) {
		if(p==t)
			t=p->rlink;
		else {
			s=p->rlink;
			boo=1;
		}
	} else {
		if(p==t)
			t=p->llink;
		else {
			s=p->llink;
			boo=1;
		}
	}
} else {
	q=p;
	s=q->rlink;
	while(s->llink!=NULL) {
		q=s;
		s=s->llink;
	}
	s->llink=p->llink;
	if (q!=p) {
		q->llink=s->rlink;
		s->rlink=p->rlink;
	}
	if (p==t)
		t=s;
	else
		boo=1;
}
if (boo==1) {
	if(p==f->llink)
		f->llink=s;
	else
		f->rlink=s;
}
free(p);
  • 5
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 《C/C++程序员面试宝典》是一本针对C/C++程序员面试准备的宝典。这本书的作者通过总结面试过程中常见的问题和经验,帮助读者提高面试技巧和应对能力。 首先,本书中提供了大量的面试题目,内容涵盖了C/C++语言的各个方面,包括数据结构、算法、指针、内存管理、多线程等。通过学习这些题目,读者可以加深对C/C++的理解,提高解决问题的能力。 其次,本书还针对不同类型的面试问题提供了详细的解答思路和讲解。这些解答思路不仅能帮助读者快速解答面试问题,还能帮助读者理解背后的原理和思想,从而更好地运用到实际开发中。 此外,本书还提供了面试中常见的技术知识点总结,帮助读者快速复习和掌握重要的概念和原理。同时,还包含了一些实际项目中常见的问题和解决方案,帮助读者在面试中展示自己的项目经验和实际应用能力。 总的来说,《C/C++程序员面试宝典》适合那些准备进行C/C++相关职位面试的程序员。通过系统地学习和准备,可以提高面试的成功率,更好地展示自己的技术能力和经验。不仅如此,本书还能作为C/C++技术的参考手册,帮助读者巩固和扩展自己的技术知识。 ### 回答2: 《C/C++程序员面试宝典》是一本专门为C/C++程序员准备的面试指南,它以全面的面试题目和详细的解析为主要特点。本书涵盖了C/C++程序员面试的各个方面,包括算法、数据结构、操作系统、网络编程、面向对象设计等内容。 本书通过多种形式的面试题目,对C/C++程序员的技术能力和经验进行全面考察。每个面试题目都提供了详细的解答和分析,帮助读者深入理解问题的解决思路,提高解决问题的能力。此外,本书还包括常见的面试问题和解答,以及面试技巧和注意事项,帮助读者在面试中更加自信和准备充分。 《C/C++程序员面试宝典》还特别强调了面试中的编程能力和实践经验。它提供了大量的编程题目和代码示例,通过实际的编程练习,帮助读者提高编程水平。此外,本书还介绍了C/C++编程规范和最佳实践,帮助读者写出高质量的代码。 总之,《C/C++程序员面试宝典》是一本非常全面、实用的面试指南。无论你是准备面试,还是提高编程技能,本书都会给你提供很大的帮助。通过学习本书,你可以系统地掌握C/C++程序员面试的各个方面,提高自己的技术能力,从而在面试中取得更好的成绩。 ### 回答3: "C/C++程序员面试宝典" 是一本针对C/C++程序员面试所编写的指导手册。这本书的目的是帮助程序员准备面试时可能遇到的各种问题和考点,包括面试常见问题、算法和数据结构、编程知识和技巧以及系统设计等内容。 这本宝典可以帮助准备面试的程序员了解面试官可能关注的重点和考察的方向。通过学习这本书,程序员可以更好地了解C/C++编程语言以及相关的编程概念和工具。书中还包含了许多典型的面试问题和答案示例,这可以帮助程序员在面试中更加自信地回答问题。 除了指导面试准备外,这本宝典还介绍了一些常见的编程算法和数据结构,以及如何应用它们解决问题。这对于程序员提高编程能力和解决实际问题非常有帮助。此外,宝典还提供了一些编程技巧和注意事项,帮助程序员编写更加高效、可维护和可读的代码。 总的来说,"C/C++程序员面试宝典" 是一本非常实用的指导手册,能够帮助准备C/C++程序员面试的人士更好地准备、展示自己的技能和知识。它提供了丰富的面试问题和答案示例,介绍了常见的算法和数据结构,并提供了编程技巧和注意事项。这本书对于求职者来说是一本不可或缺的工具书。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值