我学习的第一门编程语言是Java,所以之前一直没有系统学习过C语言。这篇文章主要就是我学习过程的一个总结,方便以后复习查看。
一. C语言初识与简单介绍
1972年,贝尔实验室的丹尼斯·里奇(Dennis Ritch )和肯·汤普逊(Ken Thompson )在开发UNIX操作系统时设计了C语言。然而,C语言不完全是里奇突发奇想而来,他是在B语言(汤普逊发明)的基础上进行设计。至于B语言的起源,那是另一个故事。C语言设计的初衷是将其作为程序员使用的一种编程工具,因此,其主要目标是成为有用的语言。
在过去40多年里,C语言已成为最重要、最流行的编程语言之一。它的成长归功于使用过的人都对它很满意。过去20多年里,虽然许多人都从C语言转而使用其他编程语言(如,C 、Objective C、Java等),但是C语言仍凭借自身实力在众多语言中脱颖而出。在学习C语言的过程中,会发现它的许多优点(见图1.1)。下面,我们来看看其中较为突出的几点。
C是高效的语言。在设计上,它充分利用了当前计算机的优势,因此C程序相对更紧凑,而且运行速度很快。实际上,C语言具有通常是汇编语言才具有的微调控制能力(汇编语言是 为特殊的中央处理单元设计的一系列内部指令,使用助记符来表示;不同的CPU系列使用不同的汇编语言),可以根据具体情况微调程序以获得最大运行速度或最有效地使用内存。
虽然这些年来C 、python和JAVA非常流行,但是C语言仍是软件业中的核心技能。在最想具备的技能中,C语言通常位居前十。特别是,C语言已成为嵌入式系统编程的流行语言。也就是说,越来越多的汽车、照相机、DVD播放机和其他现代化设备的微处理器都用C语言进行编程。
使用VS创建并运行一个hello world! 1.打开VS,点击创建新项目,来创建一个空项目。
2.输入项目名称,选择好目录后点击创建。
3.右击源文件、之后点添加、新建项
4.创建HelloWorld.c
5.编写如下代码,并点击**开始执行(不调试)**按钮。
这是程序的第1行。#include <stdio.h>
的作用相当于把stdio.h 文件中的所有内容都输入该行所在的位置。实际上,这是一种“拷贝- 粘贴”的操作。include 文件提供了一种方便的途径共享许多程序共有的信息。所有的C编译器软件包都提供stdio.h 文件。该文件中包含了供编译器使用的输入和输出函数(如,printf() )信息。该文件名的含义是标准输入 /输出头文件 。通常,在C程序顶部的信息集合被称为头文件。
int main() 是主函数,程序从这里开始执行。
到这里,就完成了任何一门语言学习的第一步。
二. 数据类型及语句
1. C语言的注释
多行注释:一种是以/*
开始、以*/
结束的块注释;
/*
这是
注释
*/
单行注释:一种是以//
开始、以换行符结束的单行注释
//这是注释
2. C语言数据类型和关键字
基本数据类型
C语言的基本数据类型为:整型、字符型、实数型。这些类型按其在计算机中的存储方式可被分为两个系列,即整数(integer)类型和浮点数(floating-point)类型。 这三种类型之下分别是:short、int、long、char、float、double这六个关键字再加上两个符号说明符signed和unsigned就基本表示了C语言的最常用的数据类型。 下面列出了在32位操作系统下常见编译器下的数据类型大小及表示的数据范围:
类型名称 | 类型关键字 | 占字节数 | 其他叫法 | 表示的数据范围 |
---|---|---|---|---|
字符型 | char | 1 | signed char | -128 ~ 127 |
无符号字符型 | unsigned char | 1 | none | 0 ~ 255 |
整型 | int | 4 | signed int | -2,147,483,648 ~ 2,147,483,647 |
无符号整型 | unsigned int | 4 | unsigned | 0 ~ 4,294,967,295 |
短整型 | short | 2 | short int | -32,768 ~ 32,767 |
无符号短整型 | unsigned short | 2 | unsigned short int | 0 ~ 65,535 |
长整型 | long | 4 | long int | -2,147,483,648 ~ 2,147,483,647 |
无符号长整型 | unsigned long | 4 | unsigned long | 0 ~ 4,294,967,295 |
单精度浮点数 | float | 4 | none | 3.4E /- 38 (7 digits) |
双精度浮点数 | double | 8 | none | 1.7E /- 308 (15 digits) |
长双精度浮点数 | long double | 10 | none | 1.2E /- 4932 (19 digits) |
长整型 | long long | 8 | __int64 | -9223372036854775808~9223372036854775808 |
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int a = 2, b = 1;//定义两个变量a,b并赋值
int c;//定义变量c用于计算a b的和
c = a b;
printf("a b = %d",c);//输出结果a b=3
return 0;
}
C语言关键字 在C语言中,为了定义变量、表达语句功能和对一些文件进行预处理,还必须用到一些具有特殊意义的字符,这就是关键字,我们用户自己定义的变量函数名等要注意不可以与关键字同名。
C语言中的32个关键字 | |||
---|---|---|---|
auto | double | int | struct |
break | else | long | switch |
case | enum | register | typedef |
char | extern | return | union |
const | float | short | unsigned |
continue | for | signed | void |
default | goto | sizeof | volatile |
do | if | static | while |
void关键字:空类型的关键字,void关键字不能定义变量,void用来修饰函数的参数或返回值,代表函数没有参数或没有返回值。
sizeof关键字:使用来测变量、数组的占用存储空间的大小(字节数)。
typedef关键字 :重命名相关的关键字。在C语言中,除系统定义的标准类型和用户自定义的结构体、共用体等类型之外,还可以使用类型说明语句typedef定义新的类型来代替已有的类型。typedef语句的一般形式是:
代码语言:javascript复制typedef 已定义的类型 新的类型;
例如:
代码语言:javascript复制typedef int INTEGER; /*指定用 INTEGER 代表 int 类型*/
typedef float REAL; /*指定用 REAL 代表 float 类型*/
在具有上述typedef语句的程序中,下列语句就是等价的:
代码语言:javascript复制int i, j; /*与 INTEGER i,j;*/
float pi; /*与 REAL pi;*/
当然typedef的最常用的作用就是给结构体变量重命名。
代码语言:javascript复制#include<stdio.h>
#include<string.h>
typedef struct _INFO
{
int num;
char str[256];
}INFO;
int main()
{
struct _INFO A;
INFO B; //通过typedef重命名后的名字INFO与struct _INFO完全等价!
A.num = 2014;
strcpy(A.str,"Welcome to dotcpp.com");
B=A;
printf("This year is %d %sn",A.num,A.str);
printf("This year is %d %sn",B.num,B.str);
return 0;
}
可以看到typedef可以为关键词改名,使改名之后的INFO类型等价于struct _INFO类型,让我们在定义这种结构类型时更方便、省事。
3. 存储相关的关键字
register、static、const、auto、extern
register 是 寄存器的意思,用register修饰的变量是寄存器变量。即:在编译的时候告诉编译器这个变量是寄存器变量,尽量将其存储空间分配在寄存器中。
注意: (1):定义的变量不一定真的存放在寄存器中。 (2):cpu 取数据的时候去寄存器中拿数据比去内存中拿数据要快 (3):因为寄存器比较宝贵,所以不能定义寄存器数组 (4):register 只能修饰 字符型及整型的,不能修饰浮点型
代码语言:javascript复制 register char ch;
register short int b;
register int c;
register float d;//错误的
(5):因为register 修饰的变量可能存放在寄存器中不存放在内存中,所以不能对寄存器变量取地址。因为只有存放在内存中的数据才有地址。
代码语言:javascript复制 register int a;
int *p;
p=&a;//错误的,a可能没有地址
static 是静态的意思,static 可以修饰全局变量、局部变量、函数。
const 是常量的意思,用const 修饰的变量是只读的,不能修改它的值。const 可以修饰指针。
代码语言:javascript复制const int a=101;//在定义 a 的时候用const 修饰,并赋初值为101
从此以后,就不能再给a赋值了。
extern 是外部的意思,一般用于函数和全局变量的声明。
4. if选择结构
if else选择程序结构用于判断给定的条件,根据判断条件的成立与否来控制程序的流程。选择结构有单选择、双选择和多选择3种形式,单选择结构用if语句实现。
形式一:
代码语言:javascript复制if(表达式) /*若条件成立则实行花括号里的语句,反之则不执行*/
{
//语句
}
形式二:
代码语言:javascript复制if(表达式) /*若表达式成立则执行语句1,否则执行语句2*/
{
//语句1
}
else
{
//语句2
}
形式三:
代码语言:javascript复制if(表达式) /*如果表达式成立,执行语句1否则继续判断表达式2*/
{
//语句1
}
else if(表达式2) /*如果表达式成立,执行语句2否则继续判断表达式3*/
{
//语句2
}
else if(表达式3) /*如果表达式成立,则执行语句3否则继续判断下一个表达式*/
{
//语句3;
}
//… …
else /*如果以上表达式都不成立 则执行语句4*/
{
//语句4
}
5. switch case语句
对于有三种或更多的结构,C语言除了用多分支选择结构else if之外,C语言还提供了switch的结构。
switch语句的执行过程为:首先计算表达式的值,然后依次与常量表达式依次进行比较,若表达式的值与某常量表达式相等,则从该常量表达式处开始执行,直到switch语句结束。若所有的常量表达式的值均不等于表达式的值,则从default处开始执行。一般形式如下:
代码语言:javascript复制switch(表达式) /*首先计算表达式的值*/
{
case 常量表达式1:语句1;
case 常量表达式2:语句2;
case 常量表达式3:语句3;
// ……
case 常量表达式n:语句n;
default:语句n 1;
}
6. break语句的用法
break,顾名思义,跳出的意思,仅用于跳出switch结构或循环结构,用于提前结束switch结构或循环。
如switch结构中,我们知道switch结构会判断从哪个case开始执行,然后接着后面所有的case后面的语句都执行完,但通常情况下我们希望仅执行一个case后面的语句,不希望输出多余的信息,因此这个时候就可以使用break语句跳出结束switch结构,如以下程序:
代码语言:javascript复制#include<stdio.h>
int main()
{
int value;
scanf("%d",&value);
switch(value)
{
case 1:printf("one");break;
case 2:printf("two");break;
case 3:printf("three");break;
default:printf("other");break;
}
return 0;
}
7. while循环语句
while语句创建一个循环,该循环在判断表达式为假(或0)之前重复执行。while语句是一个入口条件(entry-condition)循环,在进行一次循环之前决定是否要执行循环。因此有可能一次也不执行。循环的语句部分可以是一个简单语句或一个复合语句。
代码语言:javascript复制while(表达式)
{
循环体语句
}
do while语句创建一个循环,它在判断表达式为假(或0)之前重复执行。do while语句是一个退出条件循环,在执行一次循环之后才决定是否要再次执行循环,因此循环至少要被执行一次。循环的语句部分可以是一个简单语句或一个复合语句。
代码语言:javascript复制do
{
循环体语句
}while(表达式);
8. for循环语句
for语句使用由分号隔开的三个控制表达式来控制循环过程。初始化表达式只在开始执行循环语句之前执行一次。如果判断表达式为真(或非0)就执行一次循环。然后计算更新表达式并再次检查判断表达式的值。for语句是一个入口条件循环,在进行一次循环之前决定是否要执行循环,因此有可能循环一次也不执行。循环的语句部分可以是一个简单语句或一个复合语句。
代码语言:javascript复制for(初始化表达式;判断表达式;更新表达式)
{
循环体语句
}
案例:
代码语言:javascript复制#include<stdio.h>
int main()
{
int i;
for(i=0;i<20;i )
{
printf("count is %dn",i);
}
return 0;
}
9. continue语句
continue,顾名思义,是继续的意思,它仅用于循环中,用于提前结束本次循环,即跨过continue后面的循环语句,提前进入下次循环。continue只能在循环中使用!
我们可以写一个循环,从0~100,然后呢做一个if判断,如果发现是奇数就过滤掉,进入下次循环,如果是偶数就加起来。这样循环结束就是需求的结果了。
代码语言:javascript复制#include<stdio.h>
int main()
{
int n=0;
int sum=0;
for(n=0;n<100;n )
{
if(n%2!=0) //如果对2取余不等于0,说明没有整除,当然不是偶数啦
{
continue;
}
sum=sum n;
}
printf("%dn",sum);
return 0;
}
10. 格式化输出函数printf
printf函数叫做格式输出函数,其功能是按照用户指定的格式,把指定的数据输出到屏幕上,printf函数的格式为:
代码语言:javascript复制printf("格式控制字符串",输出表项);
格式字符串的形式为:% [输出最小宽度] [.精度] [长度] 类型
例如:%d
格式符表示用十进制整形格式输出,%5.2f
格式表示输出宽度为5(包括小数点),并包含2位小数。
常用的输出格式及含义如下:
格式字符 | |
---|---|
d , i | 以十进制形式输出有符号整数(正数不输出符号) |
O | 以八进制形式输出无符号整数(不输出前缀0) |
x | 以十六进制形式输出无符号整数(不输出前缀0x) |
U | 以十进制形式输出无符号整数 |
f | 以小数形式输出单、双精度类型实数 |
e | 以指数形式输出单、双精度实数 |
g | 以%f或%e中较短输出宽度的一种格式输出单、双精度实数 |
C | 输出单个字符 |
S | 输出字符串 |
*
修饰符在printf()中的用法:
假如您不想事先指定字段宽度,而是希望由程序来制定该值,那么您可以在字段宽度部分使用*代替数字来达到目的,但是您也必须使用一个参数来告诉函数宽度的值是多少。具体的说,如果转换说明符为%d,那么参数列表中应该包括一个的值和一个d的值,来控制宽度和变量的值。该技术也可以和浮点值一起使用来指定精度和字段宽度。
代码语言:javascript复制#include<stdio.h>
int main(void)
{
unsigned width,precision;
int number = 256;
double weight = 25.5;
printf("Please input number's width:n");
scanf("%d",&width);
printf("The number is: %*dn",width,number);
printf("Then please input width and precision:n");
scanf("%d %d",&width,&precision);
printf("Weight = %*.*fn",width,precision,weight);
return 0;
}
11. 类型转换
数据有不同的类型,不同类型数据之间进行混合运算时必然涉及到类型的转换问题.
自动转换的原则:占用内存字节数少(值域小)的类型,向占用内存字节数多(值域大)的类型转换,以保证精度不降低.
强制转换:通过类型转换运算来实现。(类型说明符)(表达式)
代码语言:javascript复制(float)a; // 把 a 的值转换为实型
(int)(x y); // 把 x y 的结果值转换为整型
三. 数组
数组是若干个相同类型的变量在内存中有序存储的集合。
1. 数组的分类
按元素的类型分类: 1)字符数组 即若干个字符变量的集合,数组中的每个元素都是字符型的变量 char s[10];
2)短整型的数组 short int a[10];
3)整型的数组 int a[10];
4)长整型的数组 lont int a[5];
5)浮点型的数组(单、双) float a[6]; a[4]=3.14f; double a[8]; a[7]=3.115926;
6)指针数组 char *a[10] int *a[10];
7)结构体数组 struct stu boy[10];
按维数分类: 分为一维数组和多维数组。
一维数组的定义和使用:在C语言中使用数组必须先进行定义,一维数组的定义方式:数据类型 数组名 [数组元素个数];
二维数组的定义何使用: 数据类型 数组名 [行的个数][列的个数];
多维数组定义: int a[3][4][5]
二维数组在定义的时候,可以不给出行数,但必须给出列数,二维数组的大小根据初始化的行数来定:
代码语言:javascript复制#include<stdio.h>
intmain(intargc,char*argv[])
{
inta[][3]={
{1,2,3},
{4,5,6},
{7,8,9},
{10,11,12}
};
printf("%dn",sizeof(a));
return0;
}
2. 数组的初始化
定义数组的时候,顺便给数组的元素赋初值,即开辟空间的同时并且给数组元素赋值。
一维数组的初始化
(1)全部初始化
inta[5]={2,4,7,8,5}; 代表的意思: a[0]=2;a[1]=4;a[2]=7;a[3]=8;a[4]=5;
(2) 部分初始化
int a[5]={2,4,3};初始化赋值不够后面补0 a[0]=2;a[1]=4;a[2]=3;a[3]=0;a[4]=0
二维数组的定义并初始化
(1)按行初始化:
a、全部初始化 int a[2][2]={{1,2},{4,5}}; a[0][0] =1; a[0][1] = 2; a[1][0] = 4,a[1][1]=5;
b、部分初始化 int a[3][3]={{1,2},{1}}; a[0][0] = 1;a[0][2] =0;
(1)逐个初始化:
a、全部初始化: int a [2][3]={2,5,4,2,3,4};
b、部分初始化: int a[2][3]={3,5,6,8}
四. 函数
1. 函数的分类
(1) 从定义角度分类(即函数是谁实现的)
库函数 (c库实现的) 自定义函数 (程序员自己实现的函数) 系统调用 (操作系统实现的函数)
(2) 从参数角度分类
有参函数 : 函数有形参,可以是一个,或者多个,参数的类型随便完全取决于函数的功能。
代码语言:javascript复制int fun(int a,float b,double c)
{
}
int max(int x,int y)
{
}
无参函数 : 函数没有参数,在形参列表的位置写个void或什么都不写。
代码语言:javascript复制int fun(void)
{
}
int fun()
{
}
(3) 从返回值角度分类
带返回值的函数 : 在定义函数的时候,必须带着返回值类型,在函数体里,必须有return。如果没有返回值类型,默认返回整型。
代码语言:javascript复制//定义了一个返回字符数据的函数
char fun()
{
char b='a';
return b;
}
// 如果把函数的返回值类型省略了,默认返回整型
fun()
{
return 1;
}
没返回值的函数 : 在定义函数的时候,函数名字前面加void
代码语言:javascript复制 void fun(形参表)
{
}
2. 函数的定义
函数的定义通常包含以下内容:
代码语言:javascript复制返回值类型 函数名(形参表说明) /*函数首部*/
{
说明语句 /*函数体*/
执行语句
}
例子:
代码语言:javascript复制 int max(int x, int y)
{
int z;
if(x > y){
z = x;
}else{
z = y;
}
return z;
}
3. 函数的声明
对已经定义的函数,进行说明,函数的声明可以声明多次。
有些情况下,如果不对函数进行声明,编译器在编译的时候,可能不认识这个函数,因为编译器在编译c程序的时候,从上往下编译的。
主调函数和被调函数在同一个.c文件中,被调函数在上,主调函数在下的时候就需要声明函数。如下:
代码语言:javascript复制#include <stdio.h>
void test1();
int main()
{
test1();
return 0;
}
void test1() {
printf("test1");
}
主调函数和被调函数不在同一个.c文件中的时候,将函数的声明放在头文件中,.c程序要调用,包含头文件即可。
test.c
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void test1() {
printf("test1");
}
test.h
代码语言:javascript复制extern void test1();
main.c
代码语言:javascript复制#include<stdio.h>
#include "test.h"
int main()
{
test1();
return 0;
}
4. 变量的存储类别
内存的分区:
物理内存:实实在在存在的存储设备
虚拟内存:操作系统虚拟出来的内存。
操作系统会在物理内存和虚拟内存之间做映射。在32位系统下,每个进程的寻址范围是4G,0x00000000 ~0xffffffff。在写应用程序的,咱们看到的都是虚拟地址。在运行程序的时候,操作系统会将 虚拟内存进行分区。
堆:在动态申请内存的时候,在堆里开辟内存。
栈:主要存放局部变量。
静态全局区:(1)未初始化的静态全局区:静态变量(定义变量的时候,前面加static修饰),或全局变量 ,没有初始化的,存在此区。(2)初始化的静态全局区:全局变量、静态变量,赋过初值的,存放在此区
代码区:存放咱们的程序代码
文字常量区:存放常量的。
(1)普通的全局变量
概念:在函数外部定义的变量.
代码语言:javascript复制//num 就是一个全局变量
int num=100;
int main()
{
return 0;
}
作用范围:普通全局变量的作用范围,是程序的所有地方。只不过用之前需要声明。声明方法 extern int num;。
(2) 静态全局变量 static
概念:定义全局变量的时候,前面用static 修饰。
代码语言:javascript复制//num 就是一个静态全局变量
static int num=100;
int main()
{
}
作用范围:static 限定了静态全局变量的,作用范围。**只能在它定义的.c(源文件)中有效。**在程序的整个运行过程中,一直存在。
(3)普通的局部变量
概念:在函数内部定义的,或者复合语句中定义的变量。
代码语言:javascript复制int main()
{
int num; //普通局部变量
{
int a; //普通局部变量
}
}
作用范围:在函数中定义的变量,在它的函数中有效。在复合语句中定义的,在它的复合语句中有效。在函数调用之前,局部变量不占用空间,调用函数的时候,才为局部变量开辟空间,函数结束了,局部变量就释放了。
代码语言:javascript复制#include<stdio.h>
void fun() {
int num = 3;
num ;
printf("num=%dn", num);
}
int main()
{
fun(); // num=4
fun(); // num=4
fun(); // num=4
return 0;
}
(4)静态的局部变量
概念:定义局部变量的时候,前面加static修饰。
作用范围:在它定义的函数或复合语句中有效。第一次调用函数的时候,开辟空间赋值,函数结束后,不释放,以后再调用函数的时候,就不再为其开辟空间,也不赋初值,用的是以前的那个变量。
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void fun() {
static int num = 3;
num ;
printf("num=%dn", num);
}
int main()
{
fun(); // num=4
fun(); // num=5
fun(); // num=6
return 0;
}
变量存储类别扩展:在同一作用范围内,不允许变量重名。作用范围不同的可以重名。局部范围内,重名的全局变量不起作用。(就近原则)
外部函数:咱们定义的普通函数,都是外部函数。即函数可以在程序的任何一个文件中调用。
内部函数:在定义函数的时候,返回值类型前面加static 修饰。这样的函数被称为内部函数。static 限定了函数的作用范围,在定义的.c中有效。
五. 预处理
预处理命令可以改变程序设计环境,提高编程效率,它们并不是C语言本身的组成部分,不能直接对它们进行编译,必须在对程序进行编译之前,先对程序中这些特殊的命令进行“预处理”。经过预处理后,程序就不再包括预处理命令了,最后再由编译程序对预处理之后的源程序进行编译处理,得到可供执行的目标代码。C语言提供的预处理功能有三种,分别为宏定义、文件包含和条件编译,下面将对它们进行简单介绍。
1. C语言编译过程
1):预编译
将.c 中的头文件展开、宏展开,生成的文件是.i
文件。
gcc -E hello.c -o hello.i
2):编译
将预处理之后的.i 文件生成 .s
汇编文件
gcc -S hello.i –o hello.s
3)、汇编
将.s 汇编文件生成.o
目标文件
gcc -c hello.s -o hello.o
4)、链接 将.o 文件链接成目标文件
代码语言:javascript复制 gcc hello.o -o hello_elf
2. 文件包含 #include
文件包含是C预处理程序的另一个重要功能,文件包含命令行的一般形式为:
#include <文件名>
:用尖括号包含头文件,在系统指定的路径下找头文件。
#include "文件名"
:用双引号包含头文件,先在当前目录下找头文件,找不到,再到系统指定的路径下找。
注意:include 经常用来包含头文件,可以包含 .c 文件,但是大家不要包含.c。因为include 包含的文件会在预编译被展开,如果一个.c 被包含多次,展开多次,会导致函数重复定义。预处理只是对include 等预处理操作进行处理并不会进行语法检查,这个阶段有语法错误也不会报错,第二个阶段即编译阶段才进行语法检查。
一个include命令只能指定一个被包含文件,若有多个文件要包含,则需用多个include命令。
3. 宏定义 #define
宏定义在C语言源程序中允许用一个标识符来表示一个字符串,称为“宏”,被定义为“宏”的标识符称为“宏名”。在编译预处理时,对程序中所有出现的宏名,都用宏定义中的字符串去代换,这称为“宏代换”或“宏展开”。宏定义是由源程序中的宏定义命令完成的,宏代换是由预处理程序自动完成的。在C语言中,宏分为有参数和无参数两种。
无参宏的宏名后不带参数,其定义的一般形式为:#define 标识符 字符串;
,“字符串”可以是常数、表达式、格式串等。
常常对程序中反复使用的表达式进行宏定义。例如:
代码语言:javascript复制#define M (y*y 3*y);
它的作用是指定标识符M来代替表达式(yy 3y)。在编写源程序时,所有的(yy 3y)都可由M代替,而对源程序进行编译时,将先由预处理程序进行宏代换,即用(yy 3y)表达式去置换所有的宏名M,然后再进行编译。
C语言允许宏带有参数。在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数。对于带参数的宏,在调用中,不仅要宏展开,而且要用实参去代换形参。
带参宏定义的一般形式为:#define 宏名(形参表) 字符串;
带参宏调用的一般形式为:宏名(实参表);
举例:
代码语言:javascript复制#include <stdio.h>
#define MAX(a,b) (a>b)?a:b
/*带参数的宏定义*/
main()
{
int x,y,max;
printf("input two numbers: ");
scanf("%d %d",&x,&y);
max=MAX(x,y);
printf("max=%dn",max);
/*宏调用*/
}</stdio.h>
结果如下:
代码语言:javascript复制input two numbers: 2009 2010↙
max=2010
可以看到,宏替换相当于实现了一个函数调用的功能,而事实上,与函数调用相比,宏调用更能提高C程序的执行效率。
#define 后面只有一个参数的语法: 一般情况下,宏定义时的用法为:#define a b ,后接两个参数,表示用a代替b。很多时候,#define 后只有一个参数,经常出现在头文件的开始处。
用法解释: 定义宏,并在预处理过程中将其替换为空字符串(即删除)。这样做主要是为了标记某些内容,使程序阅读者能够清楚标记表明的意义,同时又不影响被编译的源代码。也就是说,用法同define后接两个参数一样,只是后一个参数为空字符串。用途包括:(1)定义一个符号用来给#if(n)def判断。(2)多文件编译中防止头文件被重复包含。
4. 条件编译
预处理程序提供了条件编译的功能,可以按不同的条件去编译不同的程序部分,因而产生不同的目标代码文件,这对于程序的移植和调试是很有用的。条件编译可分为三种形式。
(1)第一种形式如下:
代码语言:javascript复制#ifdef 标识符
程序段 1
#else
程序段 2
#endif
它的功能是如果 标识符 已被 #define 命令定义过则对 程序段1 进行编译;否则对 程序段2 进行编译。
如果没有程序段2(为空),本格式中的#else可以没有,即可以写为:
代码语言:javascript复制#ifdef 标识符
程序段
#endif
(2)第二种形式如下:
代码语言:javascript复制#ifndef 标识符
程序段 1
#else
程序段 2
#endif
与第一种形式的区别是将 “ifdef” 改为 “ifndef” 。它的功能是如果 标识符 未被 #define 命令定义过则对 程序段1 进行编译,否则对 程序段2 进行编译。这与第一种形式的功能正好相反。
(3)第三种形式如下:
代码语言:javascript复制#if 常量表达式
程序段 1
#else
程序段 2
#endif
它的功能是如果常量表达式的值为真(非0),则对程序段1进行编译,否则对程序段2进行编译。因此可以使程序在不同的条件下完成不同的功能。
六. 指针
1. 指针的概念
系统给虚拟内存的每个存储单元分配了一个编号,从0x00000000 ~ 0xffffffff ,这个编号咱们称之为地址,指针就是地址。
指针变量:是个变量,即这个变量用来存放一个地址编号。
在32位平台下,地址总线是32位的,所以地址是32位编号,所以指针变量是32位的即4个字节。
注意: (1) 无论什么类型的地址,都是存储单元的编号,在32位平台下都是4个字节,在64位系统下,所有类型的指针都是8个字节。
(2) 对应类型的指针变量,只能存放对应类型的变量的地址。举例:整型的指针变量,只能存放整型变量的地址。
扩展: 字符变量 char ch = ‘b’; ch占1个字节,它有一个地址编号,这个地址编号就是ch的地址。 整型变量 int a = 0x12345678; a占4个字节,它占有4个字节的存储单元,有4个地址编号。
2. 指针变量的定义
(1) 简单的指针变量 : 数据类型 * 指针变量名;
int * p; //定义了一个指针变量p
在定义指针变量的时候 * 是用来修饰变量的,说明变量p是个指针变量,变量名是 p。
(2) 关于指针的运算符
用 &
: 取地址 、 用*
的指针变量来取值。
int a = 0x1234abcd;
int *p; //在定义指针变量的时候*代表修饰的意思,修饰p是个指针变量。
p = &a; //把 a 的地址给p赋值 ,&是取地址符,
p 保存了a的地址,也可以说p指向了a。
扩展:如果在一行中定义多个指针变量,每个指针变量前面都需要加*来修饰。
代码语言:javascript复制int *p,*q; //定义了两个整型的指针变量p和q
int * p,q; //定义了一个整型指针变量p,和整型的变量q
举例:
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int a = 100, b = 200;
//指针变量名是p_1而不是 *p_1 ,并且 p_1 在定义的时候没有赋初值,p_2赋了初值 &b
int *p_1, * p_2 = &b;
// p_1 可以先定义后赋值
p_1 = &a;
printf("%dn", a); // 100
printf("%dn", *p_1); // 100
printf("%dn", b); // 200
printf("%dn", *p_2); // 200
return 0;
}
(3) 指针大小 在64位系统下,所有类型的指针都是8个字节。
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
char* p1;
short* p2;
int* p3;
long* p4;
float* p5;
double* p6;
printf("%dn", sizeof(p1)); //8
printf("%dn", sizeof(p1)); //8
printf("%dn", sizeof(p3)); //8
printf("%dn", sizeof(p4)); //8
printf("%dn", sizeof(p5)); //8
printf("%dn", sizeof(p6)); //8
return 0;
}
3. 指针和变量的关系
指针可以存放变量的地址编号
代码语言:javascript复制 int a=100;
int *p;
p=&a;
在程序中,引用变量的方法 :
直接通过变量的名称 :
代码语言:javascript复制 int a;
a=100;
还可以通过指针变量来引用变量:
代码语言:javascript复制int *p; //在定义的时候,*不是取值的意思,而是修饰的意思,修饰p是个指针变量
p=&a; //取 a 的地址给p赋值,p保存了a的地址,也可以说p指向了a
*p= 100; //在调用的时候*是取值的意思,*指针变量 等价于指针指向的变量
注意:
1:*指针
取值,取几个字节,由指针类型决定的指针为字符指针则取一个字节,指针为整型指针则取4个字节,指针为double型指针则取8个字节。
2:指针
指向下个对应类型的数据。字符指针
,指向下个字符数据,指针存放的地址编号加1,整型指针
指向下个整型数据,指针存放的地址编号加4。
4. 指针和数组元素之间的关系
变量存放在内存中,有地址编号,咱们定义的数组,是多个相同类型的变量的集合,每个变量都占内存空间,都有地址编号,指针变量当然可以存放数组元素的地址。
代码语言:javascript复制int main()
{
int a[5];
// 指针变量p保存了数组a中第0个元素的地址,即a[0]的地址
int* p = &a[0];
return 0;
}
数组元素的引用方法:
(1) 方法1: 数组名[下标] :
代码语言:javascript复制 int a[5];
a[2]=100;
(2) 方法2:指针名加下标 :
代码语言:javascript复制 int a[5];
int *p;
p=a;
p[2]=100; //相当于 a[2]=100;
补充:c语言规定:数组的名字就是数组的首地址,即第0个元素的地址,就是&a[0] 。
注意:p和a的不同,p是指针变量,而a是个常量。所以可以用等号给p赋值,但不能给a赋值。
代码语言:javascript复制p=&a[3]; //正确
a=&a[3]; //错误
(3) 通过指针变量运算加取值的方法来引用数组的元素
代码语言:javascript复制 int a[5];
int *p;
p=a;
*(p 2)=100; //也是可以的,相当于a[2]=100
解释:p是第0个元素的地址,p 2是a[2]这个元素的地址。
(4) 方法4:通过数组名 取值的方法引用数组的元素
代码语言:javascript复制 int a[5];
*(a 2)=100;//也是可以的,相当于a[2]=100;
注意:a 2是a[2]的地址。这个地方并没有给a赋值。
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int a[5] = { 0,1,2,3,4 };
int* p;
p = a;
printf("a[2]=%dn", a[2]); // a[2]=2
printf("p[2]=%dn", p[2]); // p[2]=2
printf("*(p 2)=%dn", *(p 2)); // *(p 2)=2
printf("*(a 2)=%dn", *(a 2)); // *(a 2)=2
printf("p=%pn", p); // p=00000011F94FFC78
printf("p 2=%pn", p 2); // p 2=00000011F94FFC80
return 0;
}
5. 指针的运算
(1) 指针可以加一个整数,往下指几个它指向的变量,结果还是地址。 前提:指针指向数组元素的时候,加一个整数才有意义
代码语言:javascript复制 int a[5];
int *p;
p=a;
p 2;//p是a[0]的地址,p 2是&a[2]
代码语言:javascript复制 char buf[5];
char *q;
q=buf;
q 2 //相当于&buf[2]
(2) 两个相同类型指针可以比较大小。 前提:只有两个相同类型的指针指向同一个数组里面的元素的时候,比较大小才有意义。指向前面元素的指针小于指向后面元素的指针。
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int a[10];
int* p, * q; // 如果在一行上定义多个指针变量的,每个变量名前面加*
p = &a[1];
q = &a[6];
// p<q
printf("p=%dn", p); // p=-1961626100
printf("q=%dn", q); // q=-1961626080
return 0;
}
(3) 两个相同类型的指针可以做减法。 前提:必须是两个相同类型的指针指向同一个数组的元素的时候,做减法才有意义。做减法的结果是,两个指针指向的中间有多少个元素。
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int a[5];
int* p, * q;
p = &a[0];
q = &a[3];
printf("%dn", q - p); // 3
return 0;
}
结果是3。
(4) 两个相同类型的指针可以相互赋值。 前提 : 只有相同类型的指针才可以相互赋值(void*类型的除外)。
代码语言:javascript复制 int *p;
int *q;
int a;
p=&a; //p 保存 a 的地址,p指向了变量a
q=p; //用 p 给q 赋值,q也保存了a的地址,指向a
注意:如果类型不相同的指针要想相互赋值,必须进行强制类型转换。
6. 指针数组
(1) 指针和数组的关系
可以定义一个数组,数组中有若干个相同类型指针变量,这个数组被称为指针数组,如 int*p[5]
。
指针数组的概念:指针数组本身是个数组,是个指针数组,是若干个相同类型的指针变量构成的集合。
(2) 指针数组的定义方法
代码语言:javascript复制类型说明符 * 数组名 [元素个数];
举例:
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
char *name[5] = { "hello","China","beijing","project","Computer" };
int i;
for (i = 0; i < 5; i ) {
printf("%sn", name[i]);
}
return 0;
}
输出:
代码语言:javascript复制hello
China
beijing
project
Computer
“hello”、“China”“beijing” “project” “computer” 这 5 个字符串存放在文字常量区。
假设: “hello ”首地址是 0x00002000 “China”首地址是 0x00003000 “beijing”首地址是 0x00004000 “project”首地址是 0x00005000 “Computer”首地址是 0x00006000
则: name[0]中存放内容为 0x00002000 name[1]中存放内容为 0x00003000 name[2]中存放内容为 0x00004000 name[3]中存放内容为 0x00005000 name[4]中存放内容为 0x00006000
注意:name[0]、name[1]、 name[2]、 name[3]、 name[4] 分别是 char * 类型的指针变量,分别存放一个地址编号。
7. 指针的指针
指针的指针,即指针的地址,咱们定义一个指针变量本身指针变量占4个字节,指针变量也有地址编号。
例:
代码语言:javascript复制int a=0x12345678;
int *p;
p =&a;
假如:a的地址是 0x00002000。则 p中存放的是a的地址编号即 0x00002000
因为p也占4个自己内存,也有它自己的地址编号,及指针变量的地址,即指针的指针。
假如:指针变量p的地址编号是0x00003000,这个地址编号就是指针的地址。我们定义一个变量存放p的地址编号,这个变量就是指针的指针。
代码语言:javascript复制 int **q;
q=&p; //q 保存了p 的地址,也可以说q指向了p
则q里存放的就是0x00003000
还可以无限套娃:
代码语言:javascript复制int ***m;
m=&q;
8. 字符串和指针
字符串的概念:字符串就是以’ ’结尾的若干的字符的集合:比如“helloworld”。
字符串的地址,是第一个字符的地址。如:字符串“helloworld”的地址,其实是字符串中字符’h’的地址。
我们可以定义一个字符指针变量保存字符串的地址,比如:
代码语言:javascript复制char *s=”helloworld”;
字符串的存储形式: 数组、文字常量区、堆
(1) 字符串存放在数组中 其实就是在内存(栈、静态全局区)中开辟了一段空间存放字符串。
代码语言:javascript复制char str[100] = “I love C!”
定义了一个字符数组str,用来存放多个字符,并且用”I love C!”给str数组初始化 ,字符串“I love C!”存放在str中。
注: 普通全局数组,内存分配在静态全局区。 普通局部数组,内存分配在栈区。 静态数组(静态全局数组、静态局部数组),内存分配在静态全局区
(2) 字符串存放在文字常量区 在文字常量区开辟了一段空间存放字符串,将字符串的首地址给指针变量。
代码语言:javascript复制char *str = “I love C!”
定义了一个指针变量str , 只能存放字符地址编号,I love C! 这个字符串中的字符不是都存放在str指针变量中。str 只是存放了字符I的地址编号,“IloveC!”存放在文字常量区。
(3) 字符串存放在堆区
使用malloc
等函数在堆区申请空间,将字符串拷贝到堆区。
char *str =(char*)malloc(10); //动态申请了 10 个字节的存储空间,
strcpy(str,"I love C"); //首地址给str赋值,字符串“Ilove C!”拷贝到 str 指向的内存里。
字符串的可修改性:字符串内容是否可以修改,取决于字符串存放在哪里。
(1)存放在数组中的字符串的内容是可修改的(注:数组没有用const修饰)
代码语言:javascript复制 char str[100]=”I love C!”;
str[0]=‘y’; //正确可以修改的
(2)文字常量区里的内容是不可修改的
代码语言:javascript复制 char *str=”I love C!”;
*str =’y’; //错误,I 存放在文字常量区,不可修改
注:str 指向文字常量区的时候,它指向的内存的内容不可被修改。
(3) 堆区的内容是可以修改的
代码语言:javascript复制 char *str =(char*)malloc(10);
strcpy(str,"I love C");
*str=’y’; //正确,可以,因为堆区内容是可修改的
注: 1、str 指向堆区的时候,str指向的内存内容是可以被修改的。 2、str 是指针变量,也可以指向别的地方。即可以给str重新赋值,让它指向别的地方
字符数组,可以使用scanf或者strcpy函数赋值:
代码语言:javascript复制char buf[20]=”hello world”
buf="hello kitty"; //错误,因为字符数组的名字是个常量,不能用等号给常量赋值。
strcpy(buf,"hello kitty"); //正确,数组中的内容是可以修改的
scanf("%s",buf); //正确,数组中的内容是可以修改的
指针指向文字常量区时赋值:
代码语言:javascript复制char *buf_point = “hello world”;
buf_point="hello kitty"; // 正确,buf_point 指向另一个字符串
strcpy(buf_point,"hello kitty"); // 错误,这种情况,buf_point 指向的是文字常量区,内容只读。
指针指向堆区,堆区存放字符串时的赋值:
代码语言:javascript复制 char *buf_heap;
buf_heap=(char *)malloc(15);
strcpy(buf_heap,"hello world");
scanf(“%s”,buf_heap);
9. 数组指针
回顾:数组的名字是数组的首地址,是第0个元素的地址,是个常量,数组名字加1指向下个元素。
二维数组a中,a 1指向下个元素,即下一个一维数组,即下一行。
代码语言:javascript复制int main()
{
int a[3][5];
printf("a=%pn", a);
printf("a 1=%pn", a 1);
return 0;
}
数组指针的概念:本身是个指针,指向一个数组,加1跳一个数组,即指向下个数组。
数组指针的定义方法:指向的数组的类型(*指针变量名)[指向的数组的元素个数]
// 定义了一个数组指针变量p,p指向的是整型的有5个元素的数组
int (*p)[5];
例1:
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
// 定义了一个3行5列的一个二维数组
int a[3][5];
// 定义一个数组指针变量p
int(*p)[5];
printf("a=%pn", a); // 第0行的行地址
printf("a 1=%pn", a 1); // 第1行的行地址,a和a 1差20个字节
p = a;
printf("p=%pn", p); // 第0行的行地址
printf("p 1=%pn", p 1);//第1行的行地址
return 0;
}
数组指针的用法的举例:
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void fun(int(*p)[5], int x, int y) {
p[0][1] = 101;
}
int main()
{
int i, j;
int a[3][5];
fun(a, 3, 5);
for (i = 0; i < 3; i ) {
for (j = 0; j < 5; j ) {
printf("%d ", a[i][j]);
}
printf("n");
}
return 0;
}
10. 各种数组指针的定义
(1)、一维数组指针,加1后指向下一行的一维数组。用法和上一节的用法一样。
如 int (*p)[5]; ,配合每行有5个int型元素的二维数组如 int a[3][5] 、int b[4][5]、 int c[5][5]、int d[6][5]、 ……,使用 p=a;、、 p=b;、 p=c; 或者 p=d; 都是可以的。
(2)、二维数组指针,加1后指向下个二维数组 如 int (*p)[4][5]; 配合三维数组来用。三维数组中由若干个4行5列二维数组构成,如int a[3][4][5];、 int b[4][4][5];、int c[5][4][5];、 int d[6][4][5]; 这些三维数组,有个共同的特点,都是有若干个4行5的二维数组构成。可以使用 p=a;、 p=b;、 p=c;或 p=d; 都是可以的。
举例:
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int a[3][4][5];
printf("a=%pn", a);
printf("a 1=%pn", a 1);//a 和 a 1 地址编号相差 80 个字节
// 验证了a 1 跳一个4行5列的一个二维数组
int(*p)[4][5];
p = a;
printf("p=%pn", p);
printf("p 1=%pn", p 1);//p 和 p 1 地址编号相差也 80 个字节
return 0;
}
以此类推,还有三维数组指针、四维数组指针、。。。。
11. 数组名字取地址:变成数组指针
一维数组名字取地址,变成一维数组指针,即加1跳一个一维数组。
int a[10];
a 1 跳一个整型元素,是a[1]的地址。a 和a 1 相差一个元素,4个字节
&a 就变成了一个一维数组指针,是 int(*p)[10]类型的。(&a) 1 和 &a 相差一个数组即10个元素即40个字节。
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int a[10];
printf("a=%pn", a);
printf("a 1=%pn", a 1);
printf("&a=%pn", &a);
printf("&a 1=%pn", &a 1);
return 0;
}
运行结果:
在运行程序时,大家会发现a和&a所代表的地址编号是一样的,即他们指向同一个存储单元,但是a和&a的指针类型不同。
12. 数组名字和指针变量的区别
代码语言:javascript复制 int a[5];
int *p;
p=a;
相同点:
a 是数组的名字,是a[0]的地址,p=a即p保存了a[0]的地址,即a和p都指向a[0],所以在引用数组元素的时候,a和p等价。
引用数组元素回顾:a[2]、(a 2)、p[2]、(p 2) 都是对数组 a 中a[2]元素的引用。
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int a[5] = { 0,1,2,3,4 };
int* p;
p = a;
printf("a[2] = %dn", a[2]);
printf("*(a 2) = % dn", *(a 2));
printf("p[2] = %dn", p[2]);
printf("*(p 2) = % dn", *(p 2));
return 0;
}
运行结果:
不同点:
1、a是常量、p是变量。可以用等号 = 给p赋值,但是不能用等号给a赋值。 2、对a取地址,和对p取地址结果不同。因为a是数组的名字,所以对a取地址结果为数组指针。p 是个指针变量,所以对p取地址(&p)结果为指针的指针。
13. 数组指针取 *
数组指针取 *,并不是取值的意思,而是指针的类型发生变化:
一维数组指针取*,结果为它指向的一维数组第0个元素的地址,它们还是指向同一个地方。
二维数组指针取*,结果为一维数组指针,它们还是指向同一个地方。
三维数组指针取*,结果为二维数组指针,它们还是指向同一个地方。
多维以此类推。
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int a[3][5];
int(*p)[5];
p = a;
printf("a=%pn", a); //a是一维数组指针,指向第0个一维数组,即第0行
printf("*a=%pn", *a); /*a是第0行第0个元素的地址,即&a[0][0]
printf("*a 1=%pn", *a 1); //*a 1是第0行第1个元的地址,即&a[0][1]
printf("p=%pn", p); //p是一维数组指针,指向第0个一维数组,即第0行
printf("*p=%pn", *p); //*p是第0行第0个元素的地址,即&a[0][0]
printf("*p 1=%pn", *p 1); //*p 1是第0行第1个元的地址,即&a[0][1]
return 0;
}
14. 指针作为函数的参数
咱们可以给一个函数传一个整型、字符型、浮点型的数据,也可以给函数传一个地址。
例1:
代码语言:javascript复制int num;
scanf("%d",&num);
例2:
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void swap(int* p1, int* p2) {
int temp;
temp = *p1;
*p1 = *p2;
*p2 = temp;
}
int main()
{
int a = 10, b = 20;
swap(&a, &b);
printf("a=%d b=%dn", a, b);//结果为a=20 b=10
return 0;
}
结论:调用函数的时候传变量的地址,在被调函数中通过*
地址
来改变主调函数中的变量的值。
例3;
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
//此函数中改变的是p1和p2的指向,并没有给main中的a和b赋值
void swap(int* p1, int* p2) {
int* p;
p = p1;
p1 = p2;//p1=&b,让p1指向main中的b
p2 = p;//p2=&a,让p2指向main函数中a
}
int main()
{
int a = 10, b = 20;
swap(&a, &b);
printf("a=%d b=%dn", a, b);//结果为a=10 b=20
return 0;
}
总结:要想改变主调函数中变量的值,必须传变量的地址,而且还得通过 *
地址
去赋值。
例4:
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void fun(char* p) {
p = "hello kitty";
}
int main()
{
char* p = "hello world";
fun(p);
printf("%sn", p);//结果为:hello world
return 0;
}
在fun中改变的是fun函数中的局部变量p,并没有改变main函数中的变量p,所以main函数中的,变量p还是指向helloworld。
例5:
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void fun(char** p) {
*p = "hello kitty";
}
int main()
{
char* p = "hello world";
fun(&p);
printf("%sn", p);//结果为:hello kitty
return 0;
}
总结一句话:要想改变主调函数中变量的值,必须传变量的地址,而且还得通过*
地址
去赋值。无论这个变量是什么类型的。
15. 给函数传数组
给函数传数组的时候,没法一下将数组的内容作为整体传进去。只能传数组名进去,数组名就是数组的首地址,即只能把数组的地址传进去。也就是说,只能传一个4个字节大小的地址编号进去
例1:传一维数组的地址
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void fun(int *p) {
printf("%dn", p[2]); // 3
printf("%dn", *(p 3)); // 4
}
int main()
{
int a[10] = { 1,2,3,4,5,6,7,8 };
fun(a);
return 0;
}
例2:传二维数组的地址
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void fun(int(*p)[4]) {
}
int main()
{
int a[3][4] = { {1,2,3,4},{5,6,7,8},{9,10,11,12} };
fun(a);
return 0;
}
例3:传指针数组
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void fun(char** q) {
int i;
for (i = 0; i < 3; i ) {
printf("%s ", q[i]);
}
}
int main()
{
char* p[3] = { "hello","world","kitty" };
fun(p);
return 0;
}
//打印 hello world kitty
16. 指针作为函数的返回值
一个函数可以返回整型数据、字符数据、浮点型的数据,也可以返回一个指针。
例1:返回静态局部数组的地址
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
char* fun() {
char str[100] = "hello world";
return str;
}
int main()
{
char* p;
p = fun();
printf("%sn", p); // 乱码
return 0;
}
这里返回的是fun函数里面局部变量str的地址,函数运行结束,内存就被释放了,返回这个地址,也没有意义了。
更改一下,在函数里面给局部变量加static修饰,原因是,静态数组的内容,在函数结束后,亦然存在。:
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
char* fun() {
static char str[100] = "hello world";
return str;
}
int main()
{
char* p;
p = fun();
printf("%sn", p); // hello world
return 0;
}
例2:返回文字常量区的字符串的地址
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
char* fun() {
char* str = "hello world";
return str;
}
int main()
{
char* p;
p = fun();
printf("%sn", p); // hello world
return 0;
}
这里也能行,原因是文字常量区的内容,一直存在。
例3:返回堆内存的地址
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
char* fun() {
char* str;
str = (char*)malloc(100);
strcpy(str, "hello world");
return str;
}
int main()
{
char* p;
p = fun();
printf("%sn", p); // hello world
free(p);
return 0;
}
原因是堆区的内容一直存在,直到free才释放。
总结:返回的地址,地址指向的内存的内容得存在,返回的地址才有意义。
17. 指针保存函数的地址(函数指针)
(1) 函数指针的概念 咱们定义的函数,在运行程序的时候,会将函数的指令加载到内存的代码段。所以函数也有起始地址。c 语言规定:函数的名字就是函数的首地址,即函数的入口地址。咱们就可以定义一个指针变量,来存放函数的地址。这个指针变量就是函数指针变量。
(2) 函数指针的用处 函数指针用来保存函数的入口地址。在项目开发中,我们经常需要编写或者调用带函数指针参数的函数。比如Linux系统中创建多线程的函数,它有个参数就是函数指针,接收线程函数的入口地址,即创建线程成功后,新的任务执行线程函数。用于给一个函数传另一个函数进去的情况。
(3) 函数指针变量的定义
代码语言:javascript复制返回值类型(*函数指针变量名)(形参列表);
例子:
代码语言:javascript复制int fun(int x, int y) {
return 1;
}
// 定义了一个函数指针变量p, p 指向的函数。必须有一个整型的返回值,有两个整型参数。
int(*p)(int, int);
// 可以用这个p存放这类函数的地址。
p = fun;
(4) 调用函数的方法
1.通过函数的名字去调函数(最常用的)
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int fun1(int x, int y) {
return x y;
}
int main()
{
int a=fun1(1,2);
printf("%d", a); // 3
return 0;
}
2.可以通过函数指针变量去调用
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int fun2(int x, int y) {
return x y;
}
int main()
{
int (*p)(int, int);
p = fun2;
int num = (*p)(1, 2);
printf("%d", num); // 3
return 0;
}
18. 函数指针数组
概念:由若干个相同类型的函数指针变量构成的集合,在内存中连续的顺序存储。函数指针数组是个数组,它的每个元素都是一个函数指针变量。
函数指针数组的定义:
代码语言:javascript复制类型名(*数组名[元素个数])(形参列表)
例如:
代码语言:javascript复制// 定义了一个函数指针数组,数组名是p,有5个元素p[0] ~p[4],每个元素都是函数指针变量,
// 每个函数指针变量指向的函数,必须有整型的返回值,两个整型参数。
int(*p[5])(int,int);
用法举例:
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int max(int x, int y)
{
int temp;
if (x > y)
temp = x;
else
temp = y;
return temp;
}
int min(int x, int y)
{
int temp;
if (x < y)
temp = x;
else
temp = y;
return temp;
}
int add(int x, int y)
{
return x y;
}
int main()
{
int(*p[3])(int, int) = { max,min,add};
int num;
num = (*p[1])(10, 20);
printf("num=%dn", num);// num=10
return 0;
}
19. 特殊指针
(1) 空类型的指针 void *
void *
难道是指向void型的数据吗?不是,因为没有void类型的变量,void*
通用指针,任何类型的地址都可以给void*
类型的指针变量赋值。
int *p;
void *q;
q=p // 是可以的,不用强制类型转换
举例:有个函数叫memset, void * memset(void *s,int c,size_t n); ,这个函数的功能是将s指向的内存前n个字节,全部赋值为 c。它的返回值是s指向的内存的首地址,可能是不同类型的地址。所以返回值也得是通用指针。
注意:void*类型的指针变量,也是个指针变量,在32为系统下,占4个字节,64位操作系统,占8个字节。
(2) NULL 空指针
代码语言:javascript复制char *p=NULL;
咱们可以认为p哪里都不指向,也可以认为p指向内存编号为0的存储单位。一般NULL用在给指针变量初始化
20. main 函数传参
int argc
:argc 表示命令行参数的个数(argument count),包括程序本身。即 argc 的值至少为 1。
char* argv[]
:第二个参数argv,可以使用argument value来记忆,参数值的意思。argv[] 是一个指向字符串数组的指针,其中每个元素是一个指向传递给程序的参数的指针(argument vector),这些字符串是命令行参数。
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main(int argc,char *argv[])
{
int i;
printf("argc=%dn", argc);
for ( i = 0; i < argc; i )
{
printf("argv[%d]=%sn", i,argv[i]);
}
return 0;
}
main函数,不传参数运行,如下:
在编译好的exe文件的目录下,打开cmd:
代码语言:javascript复制HelloWorld.exe abc 123
七. 动态内存申请
1. 动态分配内存的概述
如果数组的长度是预先定义好的,在整个程序中固定不变,但是在实际的编程中,往往会发生这种情况,即所需的内存空间取决于实际输入的数据,而无法预先确定 。为了解决上述问题,C语言提供了一些内存管理函数,这些内存管理函数可以按需要动态的分配内存空间,也可把不再使用的空间回收再次利用。
静态分配: 1、在程序编译或运行过程中,按事先规定大小分配内存空间的分配方式。inta[10] 2、必须事先知道所需空间的大小。 3、分配在栈区或全局变量区,一般以数组的形式。 4、按计划分配。
动态分配: 1、在程序运行过程中,根据需要大小自由分配所需空间。 2、按需分配。 3、分配在堆区,一般使用特定的函数进行分配。
2. 动态分配的函数
使用动态分配的函数,需要 #include<stdlib.h>
(1) malloc 函数
函数原型: void* malloc(unsigned int size);
功能说明:在内存的动态存储区(堆区)中分配一块长度为size字节的连续区域,用来存放类型说明符指定的类型。函数原型返回 void* 指针,使用时必须做相应的强制类型转换 ,分配的内存空间内容不确定,一般使用memset 初始化。
返回值:返回分配空间的起始地址 ( 分配成功 ),返回NULL( 分配失败 ) 。
注意:在调用malloc之后,一定要判断一下,是否申请内存成功。如果多次malloc申请的内存,第1次和第2次申请的内存不一定是连续的。
举例:
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int i, * array, n;
printf("请输入您要申请的数组元素个数n");
scanf_s("%d", &n);
array = (int*)malloc(n * sizeof(int));
if (array == NULL)
{
printf("申请内存失败n");
return 0;
}
else
{
memset(array, 0, n * sizeof(int));
for (i = 0; i < n; i )
{
array[i] = i;
}
for (i = 0; i < n; i )
{
printf("array[%d]=%dn", i,array[i]);
}
free(array);//释放array指向的内存
return 0;
}
}
(2) free函数(释放内存函数)
函数原型:void free(void*ptr)
函数说明:free函数释放ptr指向的内存。注意ptr指向的内存必须是malloc、calloc、relloc动态申请的内存。
举例:
代码语言:javascript复制char* p=(char*)malloc(100);
free(p);
注意 : free后,因为没有给p赋值,所以p还是指向原先动态申请的内存。但是内存已经不能再用了,p 变成野指针了。一块动态申请的内存只能free一次,不能多次free。
(3) calloc 函数
函数原型:void* calloc(size_t nmemb,size_t size);
函数的功能:在内存的堆中,申请nmemb 块,每块的大小为size个字节的连续区域。
函数的返回值:返回 申请的内存的首地址(申请成功),返回 NULL(申请失败)。
注意:malloc 和 calloc 函数都是用来申请内存的。
区别:
- 函数的名字不一样
- 参数的个数不一样
- malloc 申请的内存,内存中存放的内容是随机的,不确定的,而calloc函数申请的内存中的内容为0
char *p=(char *)calloc(3,100);
上面代码 在堆中申请了3块,每块大小为100个字节,即300个字节连续的区域。
(4) realloc 函数(重新申请内存)
咱们调用malloc和calloc 函数,单次申请的内存是连续的,两次申请的两块内存不一定连续。
有些时候有这种需求,即我先用malloc或者calloc申请了一块内存,我还想在原先内存的基础上挨着继续申请内存。或者我开始时候使用malloc或calloc申请了一块内存,我想释放后边的一部分内存。为了解决这个问题,发明了realloc这个函数。
函数原型:void* realloc(void *s,unsigned int new_size);
函数的功能:在原先s指向的内存基础上重新申请内存,新的内存的大小为 new_size 个字节,如果原先内存后面有足够大的空间,就追加,如果后边的内存不够用,则relloc函数会在堆区找一个new_size 个字节大小的内存申请,将原先内存中的内容拷贝过来,然后释放原先的内存,最后返回新内存的地址。
如果new_size 比原先的内存小,则会释放原先内存的后面的存储空间,只留前面的newsize个字节。
返回值:新申请的内存的首地址。
举例:
代码语言:javascript复制char *p;
p=(char *)malloc(100);
//咱们想在100个字节后面追加50个字节
p=(char *)realloc(p,150); //p 指向的内存的新的大小为 150 个字节
注意:malloc、 calloc、 relloc 动态申请的内存,只有在free或程序结束的时候才释放。
3. 内存泄露
内存泄露的概念:申请的内存,首地址丢了,找不了,再也没法使用了,也没法释放了,这块内存就被泄露了。
内存泄露 例1:
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
int main()
{
char* p;
p = (char*)malloc(100);
//接下来,可以用p指向的内存了
p = "hello world"; // p指向别的地方去了
//从此以后,再也找不到你申请的100个字节了。则动态申请的100个字节就被泄露了
return 0;
}
内存泄露 例2:
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
void fun() {
char* p;
p = (char*)malloc(100);
//接下来,可以用p指向的内存了
p = "hello world"; // p指向别的地方去了
}
int main()
{
fun();
fun();
//从此以后 ,每调用一次fun泄露100个字节
return 0;
}
内存泄露 解决方案1:
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
void fun() {
char* p;
p = (char*)malloc(100);
//接下来,可以用p指向的内存了
p = "hello world"; // p指向别的地方去了
// 释放内存
free(p);
}
int main()
{
fun();
fun();
return 0;
}
内存泄露 解决方案2:
代码语言:javascript复制#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
char* fun() {
char* p;
p = (char*)malloc(100);
//接下来,可以用p指向的内存了
p = "hello world"; // p指向别的地方去了
return p;
}
int main()
{
char* q;
q = fun();
//释放内存
free(q);
return 0;
}
总结:申请的内存,一定不要把首地址给丢了,在不用的时候一定要释放内存。
八. 字符串处理函数
#pragma
指令的作用是:用于指定计算机或操作系统特定的编译器功能。
#pragma warning(disable:4996)
在c文件开始处写上这句话,即告诉编译器忽略4996警告,strcpy、scanf等一些不安全的标准c库函数在vs中可以用了。
## 1. 字符串长度函数
头文件:#include<string.h>
函数定义:size_t strlen(const char *s);
size_t 实际是无符号整型,它是在头文件中,用typedef 定义出来的。
函数功能:测字符指针s指向的字符串中字符的个数,不包括