这是我学习c语言中进行的笔记总结,每一节都有我的思想在其中,期待能帮助准备学习c语言的人一些帮助。
这是前半部分链接:C语言入门这一篇就够了!!!-万字长文(前半部分)
因为有时候我们需要多次执行同一代码块。而一般情况下,我们的程序是循序执行的,所以我们需要循环语句的存在。
C语言中循环类型分为:while循环、for循环、do…while循环、嵌套循环,这一节我们着重介绍这四种循环类型。
while循环是:只要给定的条件为真,while 循环语句会重复执行一个目标语句。
其语法为:
while(条件)
{
//程序内容;
}
下面我们用一个实例对while循环进行说明:
我们将使用 while输 1~100 以内的所有的奇数和偶数的和:
#include
int main(){
int sum=0;
int num=1;
int sum2=0;
int num2=2;
while(num<100){
sum=sum+num;
num=num+2;
}
printf("100内奇数和为:%d\n",sum);
while(num2<=100){
sum2=sum2+num2;
num2=num2+2;
}
printf("100内偶数和为:%d\n",sum2);
}
可以得到奇数和为:2500,偶数和为:2550
for循环的作用是:多次执行一个语句序列,简化管理循环变量的代码。我们可以利用for循环来指定我们需要循环的次数。其语法为:
for (循环变量赋初值; 循环条件; 循环变量增值)
{
//语句;
}
我们来解析一下这个语法:
(1)循环变量赋初值会首先被执行,且只会执行一次。这一步允许我们声明并初始化任何循环控制变量。当然也可以不在这里写任何语句,只要有一个分号出现即可。
(2)条件判断 。如果为真,则执行循环主体。如果为假,则不执行循环主体,且控制流会跳转到紧接着 for 循环的下一条语句。
(3)在执行完 for 循环主体后,控制流会跳回上面的循环变量增值语句。该语句允许您更新循环控制变量。该语句可以留空,只要在条件后有一个分号出现即可。
(4)条件再次被判断。如果为真,则执行循环,这个过程会不断重复(循环主体,然后增加步值,再然后重新判断条件)。在条件变为假时,for 循环终止。
下面我们使用一个实例对for循环进行深入了解:
我们将使用for循环输 1~100 以内的所有的奇数和偶数的和:
#include
int main(){
int sum=0;
int sum2=0;
int num,num2;
for(num=1;num<100;num=num+2){
sum=sum+num;
}
printf("奇数和%d\n",sum);
for(num2=2;num2<=100;num2=num2+2){
sum2=sum2+num2;
}
printf("偶数和%d\n",sum2);
}
和for 和 while 循环不一样,for和while是在循环头部测试循环条件。在 C 语言中,do…while 循环是在循环的尾部检查它的条件。
下面我们看一下do…while 循环的语法:
do
{
//语句;
}
while (表达式);
//注意,while 后面的分号千万不能省略。
do…while 和 while 的执行过程非常相似,唯一的区别是:“do…while 是先执行一次循环体,然后再判别表达式”。当表达式为“真”时,返回重新执行循环体,如此反复,直到表达式为“假”为止,此时循环结束。
我们用do while来求一下算法平均根:
#include
double DoSqrt(double z){
double a=1;
double b=0;
double c=0;
do{
if(b*b<z){
b+=a;
}
else{
c=b;
b-=a;
a/=10;
}
}while(a>0.000001);
return (b+c)/2;
}
int main(){
double x, y;
printf("请输入一个数字:");
scanf("%lf", &x);
if(x<0){
printf("输入错误。");
} else {
y=DoSqrt(x);
printf("%g 的平方根为: %g.\n", x, y);
}
int z=1;
do{
main();
z++;
}while(z>10);
return 0;
}
什么是嵌套循环呢?顾名思义,就是一个循环里面还有一个或多个循环。
这里我会给出for、while、do…while的嵌套循环实例,大家通过例子去理解嵌套循环。
我们来看下for循环的嵌套,我们用一个例子说明:
1)我们用for循环嵌套输出一个3×4的整数矩阵:
#include
int main()
{
int i, j;//声明两个整数变量
for(i=1; i<=3; i++){ //外层for循环
for(j=1; j<=4; j++){ //内层for循环
printf("%-4d", i*j);
}
printf("\n");
}
return 0;
}
外层 for 第一次循环时,i为1,内层 for 要输出四次 1*j 的值,也就是第一行数据;内层 for 循环结束后执行 printf(“\n”),输出换行符;接着执行外层 for 的 i++ 语句。此时外层 for 的第一次循环才算结束。
外层 for 第二次循环时,i为2,内层 for 要输出四次 2*j 的值,也就是第二行的数据;接下来执行 printf(“\n”) 和 i++,外层 for 的第二次循环才算结束。外层 for 第三次、第四次循环以此类推。
2)再将for 和 if 的嵌套使用。求 1 到 100 之间所有能被 5 整除的数之和。
# include
int main(void)
{
int i;
int sum = 0;
for (i=5; i<100; ++i)
{
if (0 == i%5)
{
sum = sum +i;
}
}
printf("sum = %d\n", sum);
return 0;
}
得到的是:950,大家可以试一下。
3)我们再用for嵌套循环得到九九乘法表:
#include
int main(){
int i, j;
for(i=1; i<=9; i++){ //外层for循环
for(j=1; j<=i; j++){ //内层for循环
printf("%d*%d=%-2d ", i, j, i*j);
}
printf("\n");
}
return 0;
}
结果为:
1*1=1
2*1=2 2*2=4
3*1=3 3*2=6 3*3=9
4*1=4 4*2=8 4*3=12 4*4=16
5*1=5 5*2=10 5*3=15 5*4=20 5*5=25
6*1=6 6*2=12 6*3=18 6*4=24 6*5=30 6*6=36
7*1=7 7*2=14 7*3=21 7*4=28 7*5=35 7*6=42 7*7=49
8*1=8 8*2=16 8*3=24 8*4=32 8*5=40 8*6=48 8*7=56 8*8=64
9*1=9 9*2=18 9*3=27 9*4=36 9*5=45 9*6=54 9*7=63 9*8=72 9*9=81
和第一个例子一样,这里内层 for 每循环一次输出一条数据,外层 for 每循环一次输出一行数据。
Note:内层 for 的结束条件是j<=i。外层 for 每循环一次,i 的值就会变化,所以每次开始内层 for 循环时,结束条件是不一样的。具体如下:
1、当 i=1 时,内层 for 的结束条件为 j<=1,只能循环一次,输出第一行。
2、当 i=2 时,内层 for 的结束条件是 j<=2,循环两次,输出第二行。
3、当 i=3 时,内层 for 的结束条件是 j<=3,循环三次,输出第三行。
4、当 i=4、5、6… 时,以此类推。
使用两层while循环画三角形:
#include
int main()
{
int i=1,j;
while (i <= 5)
{
j=1;
while (j <= i )
{
printf("%d ",j);
j++;
}
printf("\n");
i++;
}
return 0;
}
/*
其结果是:
1
1 2
1 2 3
1 2 3 4
1 2 3 4 5
*/
使用do…while嵌套循环生成三角形:
#include
int main()
{
int i=1,j;
do
{
j=1;
do
{
printf("*");
j++;
}while(j <= i);
i++;
printf("\n");
}while(i <= 9);
return 0;
}
循环控制语句是用来改变代码的执行顺序的。我们通过它实现代码的跳转。我们再这里会介绍C语言中的break、continue和goto语句。
C 语言中 break 语句有以下两种用法:
1)用于终止循环,就是当 break 语句出现在一个循环内时,循环会立即终止,且程序将继续执行紧接着循环的下一条语句。
2)用于终止 switch 语句中的一个 case。
如:我们用while循环计算1+2+3+…+100的值:
#include
int main(){
int i=1, sum=0;
while(1){ //循环条件为死循环
sum+=i;
i++;
if(i>100) break;
}
printf("%d\n", sum);
return 0;
}
这里,while 循环条件为 1,是一个死循环。当执行到第100次循环的时候,计算完i++;后 i 的值为 101,此时 if 语句的条件 i> 100 成立,执行break;语句,结束循环。
continue 语句的作用是跳过循环体中剩余的语句而强制进入下一次循环。continue语句只用在 while、for 循环中,常与 if 条件语句一起使用,判断条件是否成立。
#include
int main ()
{
/* 局部变量定义 */
int a = 10;
/* do 循环执行 */
do
{
if( a == 15)
{
/* 跳过迭代 */
a = a + 1;
continue;
}
printf("a 的值: %d\n", a);
a++;
}while( a < 20 );
return 0;
}
goto语句又叫无条件转移语句。
void main(){
int a=2,b=3;
if(a<b)
goto aa;
printf("hello");
aa:printf("s");
return 0;
}
这里我们将得到的结果hello,用goto直接转为了s,你看懂了吗?
函数就是一段封装好的,可以重复使用的代码,它使得我们的程序更加模块化,不需要编写大量重复的代码。
每个 C语言程序都至少有一个函数,即主函数 main() ,所有简单的程序都可以定义其他额外的函数。
1 函数名称
① 这是函数的实际名称。
②理论上是可以随意起名字,但最好起的名字见名知意,应该让用户看到这 个函数名字就知道这个函数的功能。注意,函数名的后面有个圆换号 (),代表这个为函数,不是普通的变量名。
2 形参列表
① 在定义函数时指定的形参,在未出现函数调用时,它们并不占内存中的存储单元,因此称它们是形式参数或虚拟参数,简称形参,表示它们并 不是实际存在的数据,所以,形参里的变量不能赋值。
② 在定义函数时指定的形参,必须是,类型+变量的形式。
③ 如果没有形参,圆括号内容为空,或写一个 void 关键字。
3 函数体:函数主体包含一组定义函数执行任务的语句。
4 返回类型
① 函数的返回值是通过函数中的 return 语句获得的,return 后面的值也可 以是一个表达式。
② 尽量保证 return 语句中表达式的值和函数返回类型是同一类型
③ 如果函数返回的类型和 return 语句中表达式的值不一致,则以函数返回类型为准,即函数返回类型决定返回值的类型。对数值型数据,可以 自动进行类型转换。
④ return 语句的另一个作用为中断 return 所在的执行函数,类似于 break 中断循环、switch 语句一样。
⑤ 如果函数带返回值,return 后面必须跟着一个值,如果函数没有返回 值,函数名字的前面必须写一个 void 关键字,这时候,我们写代码时也 可以通过 return 中断函数(也可以不用),只是这时,return 后面不带内 容( 分号“;”除外)。
注意:如果函数返回的类型和 return 语句中表达式的值不一致,而它又 无法自动进行类型转换,程序则会报错。
根据函数能否被其他源文件调用,将函数区分为内部函数和外部函数。
内部函数又称静态函数。使用内部函数,可以使函数的作用域只局限于所在文件。即使在不同的文件中有同名的内部函数,也互不干扰。提高了程序的可靠性。如果一个函数只能被本文件中其他函数所调用,它称为内部函数。在定义内部函数时,在函数名和函数类型的前面加 static,即
static 类型名 函数名 (形参表)
例如:
static int max(int a,int b)
如果在定义函数时,在函数的首部的最左端加关键字 extern,则此函数是外部函数,可供其它文件调用。
如函数首部可以为:
extern int max (int a,int b)
C 语言规定,如果在定义函数时省略 extern,则默认为外部函数。
在需要调用此函数的其他文件中,需要对此函数作声明(不要忘记,即使在本文件中调用一个函数,也要用函数原型来声明)。在对此函数作声明时,要加关键字 extern,表示该函数是在其他文件中定义的外部函数。
当调用函数时,需要关心5要素:
函数声明包括以下几个部分:
return_type function_name( parameter list );
在函数声明中,参数的名称并不重要,只有参数的类型是必需的,因此下面也是有效的声明:
int max(int, int);
当您在一个源文件中定义函数且在另一个文件中调用函数时,函数声明是必需的。在这种情况下,您应该在调用函数的文件顶部声明函数。
创建 C 函数时,会定义函数做什么,然后通过调用函数来完成已定义的任务。
当程序调用函数时,程序控制权会转移给被调用的函数。被调用的函数执行已定义的任务,当函数的返回语句被执行时,或到达函数的结束括号时,会把程序控制权交还给主程序。
实例-求三个数的最大值
#include
int DoMax(int a, int b, int c){
int max=a;
if(b>max){
max=b;
if(c>max){
max=c;
}
} else {
if(c>max){
max=c;
}
}
return max;
}
int main(){
int x, y, z, maxOne;
printf("请输入三个数字:");
scanf("%d%d%d",&x,&y,&z);
maxOne=DoMax(x, y, z);
printf("\n");
printf("最大数为:%d; \n",maxOne);
return 0;
}
#include
time_t time(time_t *t);
功能:获取当前系统时间
参数:常设置为NULL
返回值:当前系统时间, time_t 相当于long类型,单位为毫秒
#include
void srand(unsigned int seed);
功能:用来设置rand()产生随机数时的随机种子
参数:如果每次seed相等,rand()产生随机数相等
返回值:无
#include
int rand(void);
功能:返回一个随机数值
参数:无
返回值:随机数
#include
#include
#include
int main()
{
time_t tm = time(NULL);//得到系统时间
srand((unsigned int)tm);//随机种子只需要设置一次即可
int r = rand();
printf("r = %d\n", r);
return 0;
}
在 C 语言中,形参与实参虽然很简单,但是,是初学者比较容易混淆的一个点。这里给予我的一点理解:
1、从字面上理解,所谓形式参数即只只是声明了一个作为参数的变量,并未直接进行赋值使用,而实际参数则相反。
在调用函数过程中发生的实参与形参之间的数据传递,常称为“虚实结合”。
2、在定义函数中制定的形参,在没有出现函数调用时不占用内存中的存储单元。在函数调用时才分配内存.
3、程序运行时是将实参的值传递给形参。
4、在执行函数时,由于形参已经有值。可以用形参进行运算。
5、通过return语句将函数值返回,若无返回值,则无return。
6、调用结束后,形参被释放掉,实参保留原值(单向传值)。
例:
#include
int test(int a,int b); // 形参,只声明
int main()
{
printf("%d",test(5,3)); // 实参,已赋值
}
int test(int a,int b) // 形参
{
a=a+b;
return a;
}
#include
void swap(int x, int y)
{
int temp;
temp = x;
x = y;
y = temp;
}
int main( int argc, char *argv[] )
{
int a = 5;
int b = 10;
swap(a, b); //调用交换函数
printf("交换结果为 a = %d, b = %d\n",a,b);
return 0;
}
由于值传递是单向传递,传递过程中只是改变了形参的数值,并未改变实参的数值,因此并不会改变a和b原有的值。即a=5,b=10
#include
void swap(int *x, int *y)
{
int temp;
temp = *x;
*x = *y;
*y = temp;
}
int main( int argc, char *argv[] )
{
int a = 5;
int b = 10;
swap(&a, &b); //调用交换函数
printf("交换结果为 a = %d, b = %d\n",a,b);
return 0;
}
指针传递过程中,将a和b的地址分别传递给了x和y,在函数体内部改变了a、b所在地址的值,即交换了a、b的数值。即a=10,b=5
#include
void swap(int &x, int &y)
{
int temp;
temp = x;
x = y;
y = temp;
}
int main( int argc, char *argv[] )
{
int a = 5;
int b = 10;
swap(a, b); //调用交换函数
printf("交换结果为 a = %d, b = %d\n",a,b);
return 0;
}
引用传递中,在调用swap(a, b);时函数会用a、b分别代替x、y,即x、y分别引用了a、b变量,这样函数体中实际参与运算的其实就是实参a、b本身,因此也能达到交换数值的目的。
注:严格来说,C语言中是没有引用传递,这是C++中语言特性,因此在.c文件中使用引用传递会导致程序编译出错。
如果使用用户自己定义的函数,而该函数与调用它的函数(即主调函数)不在同一文件中,或者函数定义的位置在主调函数之后,则必须在调用此函数之前对被调用的函数作声明。
&emsp所谓函数声明,就是在函数尚在未定义的情况下,事先将该函数的有关信息通知编译系统,相当于告诉编译器,函数在后面定义,以便使编译能正常进行。
注意:一个函数只能被定义一次,但可以声明多次。
谈到函数的定义和声明,就可以引出C语言的作用域,其分为全局和局部,由此引出的是全局变量和局部变量。
全局变量是定义在函数外部,通常将其置于程序的顶部。全局变量在整个程序生命周期内都是有效的,在任意的函数内部能访问全局变量。
在某个函数或块的内部声明的变量称为局部变量。它们只能被该函数或该代码块内部的语句使用。局部变量在函数外部是不可知的。
学习数组的时候我们需要思考的一个事情就是:
为什么需要数组?
1、为了解决大量同类型数据的存储和使用问题
2、为了模拟现实世界
数组就是在内存中连续的相同类型的变量空间。同一个数组所有的成员都是相 同的数据类型,同时所有的成员在内存中的地址是连续的。

怎么定义一维数组?
1、为n个变量连续分配存储空间
2、所有变量数据类型必须相同
3、所有变量所占字节大小必须相等
4、数组名字符合标识符的书写规定(数字、英文字母、下划线)
5、同一作用域内,数组名不能与其它变量名相同
例:
int a[5];//方括号[]中常量表达式表示数组元素的个数
定义数组时[]内最好是常量,使用数组时[]内即可是常量,也可以是变量
值得我们注意的是:
一维数组名并不代表数组中所有的元素
一维数组名代表数组第一个元素的地址
在定义数组的同时进行赋值,称为初始化。全局数组若不初始化,编译器将其初始化为零。局部数组若不初始化,内容为随机值。
完全初始化
int a[5]={1,2,3,4,5};
不完全初始化
int a[5]={1,2,3};
//这里没有被初始化的元素自动为0
不初始化
int a[5];
//这里元素都是垃圾值
清零
int a[5]={0};
int main()
{
int a[] = { 1, 2, 3,4, 5, 6, 7, 8, 9, 10 };//定义一个数组,同时初始化所有成员变量
int i = 0;
int j = sizeof(a) / sizeof(a[0]) - 1;
int tmp;
while (i < j)
{
tmp = a[i];
a[i] = a[j];
a[j] = tmp;
i++;
j--;
}
for (i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
这里我们提供的数组为:{ 1, 2, 3,4, 5, 6, 7, 8, 9, 10 },逆置后为:

二维数组定义的一般形式为:类型说明符 数组名[常量表达式1][常量表达式2],其中常量表达式1表示第一维下标的长度,常量表达式2 表示第二维下标的长度。
二维数组的声明:
type arrayName [ x ][ y ];
例:
int a[3][4];
这里总共能够得到3行4列,12个元素

二维数组初始化:
//方法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}
};
二维数组在概念上是二维的:其下标在两个方向上变化,对其访问一般需要两个下标。
在内存中并不存在二维数组,二维数组实际的硬件存储器是连续编址的,也就是说内存中只有一维数组,即放完一行之后顺次放入第二行,和一维数组存放方式是一样的。
这里涉及到一个问题,就是是否存在多维数组?
答案是不存在的,因为内存是线性的一维的
n维数组可以当做每个元素是n-1维数组的一维数组
如:
int a[3][4]
//该数组可以看成含有三个元素的一维数组,每个数组有4个元素
int a[3][4][5]
//该数组可以看成含有三个元素的一维数组,每个元素都是4行5列的二维数组
我们用一个程序进行说明:
假如我们有一个数组a[5]={1,2,3,4,5},我们如何去访问它呢?
我们能通过循环去访问
#include
int main ()
{
int a[ 5 ]={1,2,3,4,5};
for ( int i = 0; i < 5; i++ )
{
printf("i = %d\n", i);
}
return 0;
}
给函数传一个一维数组有三种方式:
//1、形参是指针
void func(int *arr)
{
}
//2、形参是定义大小的数组
void func(int arr[10])
{
}
//2、形参是未定义大小的数组
void func(int arr[])
{
}
下面我们给出一个实例加深理解:
#include
int getAverage(int arr[], int size)
{
int sum=0;
for (int i = 0; i < size; i++)
{
sum += arr[i];
}
int Average = sum / size;
return Average;
}
int main ()
{
/* 带有 5 个元素的整型数组 */
int array[5] = {10, 20, 30, 40, 50};
/* 传递一个指向数组的指针作为参数 */
int Aver = getAverage( array, 5 ) ;
/* 输出返回值 */
printf( "平均值是: %d ", Aver );
return 0;
}
//结果是30
右值(Rvalues):术语右值(Rvalues)指的是存储在内存中某些地址的数值。右值是不能对其进行赋值的表达式,也就是说,右值可以出现在赋值号的右边,但不能出现在赋值号的左边。
变量是左值,因此可以出现在赋值号的左边。数值型的字面值是右值,因此不能被赋值,不能出现在赋值号的左边。
C 语言中没有字符串这种数据类型,可以通过 char 的数组来替代;
字符串一定是一个 char 的数组,但 char 的数组未必是字符串;
以数字 0(和字符‘\0’等价)结尾的 char 数组就是一个字符串,但如果 char 数组没有以数字 0 结尾,那么就不是一个字符串,只是普通字符数组,所以字符串是一种特殊的 char 的数组。
采用一个小例子进行说明:
#include
#include
int main()
{
char c1[] = { 'h', ' ', 'e', 'l', 'l', 'o' }; //普通字符数组
printf("c1 = %s\n", c1);//乱码,因为没有’\0’结束符
char c2[] = { 'h', 'e', 'l', 'l', 'o','\0'}; //带\0为字符串
printf("c2 = %s\n", c2);
//字符串处理以‘\0’(数字 0)作为结束符,后面的'w', 'o', 'r', 'l', 'd',不会输出
char c3[] = { 'h', 'e', 'l', 'l', 'o', '\0', 'w', 'o', 'r', 'l', 'd', '\0'};
printf("c3 = %s\n", c3);
system("pause");
return 0;
}
#include
int main()
{
//不指定长度, 没有0结束符,有多少个元素就有多长
char c1[] = { 'm', 'a', 'k' };
printf("c1 = %s\n", c1); //乱码
//指定长度,后面没有赋值的元素,自动补0
char c2[100] = { 'm', 'a', 'k','e'};
printf("c2 = %s\n", c2);
//所有元素初始化为0
char c3[100] = { 0 };
//char c4[2] = { '1', '2', '3' };//数组越界
//遇到字符0不会结束
char c5[50] = { 'h', 'e', 'l', '0', 'l' };
printf("c5 = %s\n", c5);
//遇到\0会结束
char c6[50] = { 'h', 'e', 'l', '\0', 'l' };
printf("c6 = %s\n", c6);
//遇到数字0会结束
char c7[50] = { 'h', 'e', 'l', 0, 'l' };
printf("c7 = %s\n", c7);
}
return 0;

//使用字符串初始化,编译器自动在后面补0,常用
char c8[] = "abchdkdo";
//'\0'后面最好不要连着数字,有可能几个数字连起来刚好是一个转义字符
//'\ddd'八进制字义字符,'\xdd'十六进制转移字符
// \012相当于\n
char str[] = "\012abc";
printf("str == %s\n", str);
return 0;
}
#define _CRT_SECURE_NO_WARNINGS
#include
int main()
{
char str[100];
printf("input string1 : \n");
scanf("%s", str);//scanf(“%s”,str)默认以空格分隔
printf("output:%s\n", str);
return 0;
}
内存含义:
存储器:计算机的组成中,用来存储程序和数据,辅助 CPU 进行运算处理 的重要部分。
内存:内部存贮器,暂存程序/数据——断电会丢失 SRAM、DRAM、DDR、 DDR2、DDR3。
外存:外部存储器,长时间保存程序/数据—断电不丢 ROM、ERRROM、FLASH (NAND、NOR)、硬盘、光盘。
内存是沟通 CPU 与硬盘的桥梁:
暂存放 CPU 中的运算数据。
暂存与硬盘等外部存储器交换的数据。
具体可参考计算机组成原理-第三章-存储系统(1)-基本概念
有关内存的两个概念:物理存储器和存储地址空间。
物理存储器:实际存在的具体存储器芯片。
① 主板上装插的内存条
② 显示卡上的显示 RAM 芯片
③ 各种适配卡上的 RAM 芯片和 ROM 芯片
存储地址空间:对存储器编码的范围。我们在软件上常说的内存是指这一层含义。
编码:对每个物理存储单元(一个字节)分配一个号码
寻址:可以根据分配的号码找到相应的存储单元,完成数据的读写
将内存抽象成一个很大的一维字符数组。
编码就是对内存的每一个字节分配一个 32 位或 64 位的编号(与 32 位或者 64 位处理器相关)。
这个内存编号我们称之为内存地址。
内存区的每一个字节都有一个编号,这就是“地址”。
如果在程序中定义了一个变量,在对程序进行编译或运行时,系统就会给这个变量分配内存单元,并确定它的内存地址(编号)
指针的实质就是内存“地址”。指针就是地址,地址就是指针。
指针是内存单元的编号,指针变量是存放地址的变量。
注意:通常我们叙述时会把指针变量简称为指针,实际他们含义并不一样。
指针也是一种数据类型,指针变量也是一种变量
指针变量指向谁,就把谁的地址赋值给指针变量
*操作符操作的是指针变量指向的内存空间
#include
int main()
{
int a = 0;
char b = 100;
printf("%p, %p\n", &a, &b);
//打印 a, b 的地址
//int *代表是一种数据类型,int*指针类型,p 才是变量名
//定义了一个指针类型的变量,可以指向一个 int 类型变量的地址
int *p; p = &a;//将 a 的地址赋值给变量 p,p 也是一个变量,值是一个内存地址编号
printf("%d\n", *p);//p 指向了 a 的地址,*p 就是 a 的值
char *p1 = &b; printf("%c\n", *p1);//*p1 指向了 b 的地址,*p1 就是 b 的值
return 0;
}
#include
int main()
{
int a = 0;
int b = 11;
int *p = &a; //p指向a的地址
*p = 100; //将a的值改为100
printf("a = %d, *p = %d\n", a, *p);
return 0;
}
使用 sizeof()测量指针的大小,得到的总是:4 或 8
sizeof()测的是指针变量指向存储地址的大小
在 32 位平台,所有的指针(地址)都是 32 位(4 字节)
在 64 位平台,所有的指针(地址)都是 64 位(8 字节)
我们用下面这段代码来验证:
#include
int main()
{
int *p1;
int **p2;
char *p3;
char **p4;
printf("sizeof(p1) = %d\n", sizeof(p1));
printf("sizeof(p2) = %d\n", sizeof(p2));
printf("sizeof(p3) = %d\n", sizeof(p3));
printf("sizeof(p4) = %d\n", sizeof(p4));
printf("sizeof(double *) = %d\n", sizeof(double *));
return 0;
}

指针变量也是变量,是变量就可以任意赋值,不要越界即可(32 位为 4 字节, 64 位为 8 字节),但是,任意数值赋值给指针变量没有意义,因为这样的指针就成了野指针,此指针指向的区域是未知(操作系统不允许操作此指针指向的内存区域)。所以,野指针不会直接引发错误,操作野指针指向的内存区域才 会出问题。
#include
int main()
{
int a = 100;
int *p;
p = a; //把 a 的值赋值给指针变量 p,p 为野指针, ok,不会有问题,但没有意义
p = 0x12345678; //给指针变量 p 赋值,p 为野指针, ok,不会有问题,但没有意义
*p = 1000; //操作野指针指向未知区域,内存出问题,err
return 0;
}
但是,野指针和有效指针变量保存的都是数值,为了标志此指针变量没有指向任何变量(空闲可用),C 语言中,可以把 NULL 赋值给此指针,这样就标志此指 针为空指针,没有任何指针。int *p = NULL;
#define NULL ((void *)0)void *指针可以指向任意变量的内存空间:
#include
int main()
{
void *p = NULL;
int a = 10;
p = (void *)&a; //指向变量时,最好转换为 void *
//使用指针变量指向的内存时,转换为 int * *( (int *)p ) = 11;
printf("a = %d\n", a);
return 0;
}
#include
int main()
{
int a = 10;
int b = 20;
//指向常量的指针
//修饰*,指针指向内存区域不能修改,指针指向可以变
const int *p1 = &a; //等价于 int const *p1 = &a;
//*p1 = 111; //err
p1 = &b; //ok
//指针常量
//修饰 p1,指针指向不能变,指针指向的内存可以修改
int * const p2 = &a;
//p2 = &b; //err
*p2 = 222; //ok
return 0;
}
① 数组名:
数组名字是数组的首元素地址,但它是一个常量
② 指针操作数组元素
#include
int main()
{
int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int i = 0;
int n = sizeof(a) / sizeof(a[0]);
for (i = 0; i < n; i++)
{
//printf("%d, ", a[i]);
printf("%d, ", *(a+i));
}
printf("\n");
int *p = a; //定义一个指针变量保存 a 的地址
for (i = 0; i < n; i++) p[i] = 2 * i;
for (i = 0; i < n; i++) printf("%d, ", *(p + i));
printf("\n");
return 0;
}
③ 指针加法运算
指针计算不是简单的整数相加
如果是一个 int * ,+1 的结果是增加一个 int 的大小
如果是一个 char * ,+1 的结果是增加一个 char 大小
通过一个小测验说明:
#include
int main()
{
int a;
int *p = &a;
printf("%d\n", p);
p += 2;//移动了 2 个 int
printf("%d\n", p);
//-------------------------------
char b = 0;
char *p1 = &b;
printf("%d\n", p1);
p1 += 2;//移动了 2 个 char
printf("%d\n", p1);
return 0;
}
我测出来的为(不唯一,只为了去了解这个知识点):
2114946764
2114946772
2114946763
2114946765
当然我们也能通过改变指针指向操作数组元素:
#include
int main()
{
int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int i = 0;
int n = sizeof(a) / sizeof(a[0]);
int *p = a; //指向数组首地址
for (i = 0; i < n; i++)
{
printf("%d, ", *p);
p++;
}
printf("\n");
return 0;
}
④ 指针减法运算
逆序输出:
#include
int main()
{
int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int i = 0;
int n = sizeof(a) / sizeof(a[0]);
int *p = a+n-1;
for (i = 0; i < n; i++)
{
printf("%d, ", *p);
p--;
}
printf("\n");//9, 8, 7, 6, 5, 4, 3, 2, 1,
return 0;
}
⑤ 指针数组
指针数组,它是数组,数组的每个元素都是指针类型。
#include
int main()
{
//指针数组
int *p[3];
int a = 1;
int b = 2;
int c = 3;
int i = 0;
p[0] = &a;
p[1] = &b;
p[2] = &c;
for (i = 0; i < sizeof(p) / sizeof(p[0]); i++ )
{
printf("%d, ", *(p[i]));
}
printf("\n");
return 0;
}
C 语言允许有多级指针存在,在实际的程序中一级指针最常用,其次是二级 指针。
二级指针就是指向一个一级指针变量地址的指针。
三级指针基本用不着。
① 函数形参改变实参的值
经典案例:交换
#include
void swap1(int x, int y)
{
int tmp;
tmp = x;
x = y;
y = tmp;
printf("x = %d, y = %d\n", x, y); //x = 5, y = 3
}
void swap2(int *x, int *y)
{
int tmp;
tmp = *x;
*x = *y;
*y = tmp;
}
int main()
{
int a = 3;
int b = 5;
swap1(a, b); //值传递
printf("a = %d, b = %d\n", a, b); //a = 3, b = 5,未达到交换
a = 3; b = 5;
swap2(&a, &b); //地址传递
printf("a2 = %d, b2 = %d\n", a, b);//a2 = 5, b2 = 3,成功交换
return 0;
}
② 数组名做函数参数
数组名做函数参数,函数的形参会退化为指针:
#include
//void printArrary(int a[10], int n)
//void printArrary(int a[], int n)
void printArrary(int *a, int n)
{
int i = 0;
for (i = 0; i < n; i++)
{
printf("%d, ", a[i]);
}
printf("\n");
}
int main()
{
int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int n = sizeof(a) / sizeof(a[0]);
//数组名做函数参数
printArrary(a, n);//1, 2, 3, 4, 5, 6, 7, 8, 9,
return 0;
}
③ 指针做为函数的返回值
#include
int a = 10;
int *getA() //这里的数据类型是:指针类型
{
return &a; //所以返回的是地址
}
int main()
{
*( getA() ) = 100;
printf("a = %d\n", a);
return 0;
}
函数指针是指向函数的指针变量。函数指针可以像一般函数一样,用于调用函数、传递参数。
函数指针变量的声明:
typedef int (*funPtr)(int,int);
示例:
#include
int sum(int a, int b) {
return a + b;
}
int main(void) {
/* ptr 是函数指针 */
int (* ptr)(int, int) = & sum; // &可以省略
int a = 10, b = 50 , c = 100;
/* 与直接调用函数等价,d = sum(sum(a, b), c) */
int d = ptr(ptr(a, b), c);
printf("a + b + c = %d", d);
return 0;
}
回调函数
函数指针作为某个函数的参数,也就是一个函数执行时调用另外一个已经实现的函数。
示例:
#include
int add(int a,int b){
return a + b;
}
//sum回调add函数
int sum(int num,int (*add)(int,int),int a,int b){
return num + add(a,b);
}
int main()
{
int (* p)(int a,int b)= &add;
printf("ADD = %d\n",add(1,2));
printf("SUM= %d\n",sum(2,add,1,2));
return 0;
}
① 字符指针
#include
int main()
{
char str[] = "hello world";
char *p = str;
*p = 'm';
p++;
*p = 'i';
printf("%s\n", str);
p = "mike jiang";
printf("%s\n", p);
char *q = "test";
printf("%s\n", q);
return 0;
在 C 语言中,字符串实际上是使用空字符 \0 结尾的一维字符数组。因此,\0 是用于标记字符串的结束。
空字符(Null character)又称结束符,缩写 NULL,是一个数值为 0 的控制字符,\0 是转义字符,意思是告诉编译器,这不是字符 0,而是空字符。
创建一个Hello字符串:char str[6] = {'H', 'e', 'l', 'l', 'o', '\0'};,也可以写成:char str[] = "Hello";,
示例:
#include
int main ()
{
char str[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
char str1[] = "World";
printf("str = %s\n", str );
printf("str1 = %s\n", str1 );
return 0;
}
获取字符串的长度(strlen)
size_t strlen( const char* str);
typedef unsigned int size_t。字符串复制或赋值(strcpy)
char *strcpy(char* dest, const char* src);
字符串复制或赋值(strncpy)
char * strncpy(char* dest,const char* src, const size_t n);
字符串拼接(strcat)
char *strcat(char* dest,const char* src);
字符串拼接(strncat)
char *strncat (char* dest,const char* src, const size_t n);
字符串比较(strcmp、strncmp)
int strcmp(const char *str1, const char *str2 );
int strncmp(const char *str1,const char *str2 ,const size_t n);
两个字符串比较的方法是比较字符的ASCII码的大小,从两个字符串的第一个字符开始,如果分不出大小,就比较第二个字符,如果全部的字符都分不出大小,就返回0,表示两个字符串相等。
字符查找(strchr、strrchr)
char *strchr(const char *s,const int c);
char *strrchr(const char *s,const int c);
字符串查找(strstr)
char *strstr(const char* str,const char* substr);
示例:
#include
#include
int main ()
{
char str1[14] = "hello";
char str2[14] = "world";
char str3[14];
int len ;
/* 复制 str1 到 str3 */
strcpy(str3, str1);
printf("strcpy( str3, str1) : %s\n", str3 );
/* 连接 str1 和 str2 */
strcat( str1, str2);
printf("strcat( str1, str2): %s\n", str1 );
len = strlen(str1);
printf("strlen(str1) : %d\n", len );
return 0;
}
数组允许定义可存储相同类型数据项的变量,结构体则是一种用户自定义的可用的数据类型,你可以用它存储不同类型的数据项。
&emsp可以这样说:结构体是一个集合,是一种程序员自己可以构造的数据类型,如:我们构造一个人的数据类型,它应该有姓名,年龄,身高等变量,如:
struct st_girl
{
char name[20]; // 姓名
int age; // 年龄
int height; // 身高
};
结构体存储:
结构体中成员变量分配的空间是按照成员变量中占用空间最大的来作为分配单位,同样成员变量的存储空间也是不能跨分配单位的,如果当前的空间不足,则会存储到下一个分配单位中。
结构体内存大小对齐原则:
共用体也叫联合体,它是一种特殊的数据类型,允许你在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。
由于共用体是在相同的内存位置存储不同的数据类型,所以,常利用其进行机器大小端判断!
结构体与共用体不同之处:
C语言中可以通过typedef 关键字为类型取一个新的名字。如:
typedef long l;//将long类型取别名为l
typedef的特点:
typedef 与 #define 的区别
#define可以使用其他类型说明符对宏类型名进行扩展,但对 typedef 所定义的类型名却不能这样做。
在连续定义几个变量的时候,typedef 能够保证定义的所有变量均为同一类型,而 #define 则无法保证。
C 预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。简言之,C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。我们将把 C 预处理器(C Preprocessor)简写为 CPP。
C语言由源代码生成可执行程序的过程如下:
C源程序->编译预处理->编译->优化程序->汇编程序->链接程序->可执行文件
(1)预编译
主要处理源代码文件中的以“#”开头的预编译指令。处理规则见下:
删除所有的#define,展开所有的宏定义。
处理所有的条件预编译指令,如“#if”、“#endif”、“#ifdef”、“#elif”和“#else”。
处理“#include”预编译指令,将文件内容替换到它的位置,这个过程是递归进行的,文件中包含其他 文件。
删除所有的注释,“//”和“/**/”。
保留所有的#pragma 编译器指令,编译器需要用到他们,如:#pragma once 是为了防止有文件被重复引用。
添加行号和文件标识,便于编译时编译器产生调试用的行号信息,和编译时产生编译错误或警告是 能够显示行号。
(2)编译
把预编译之后生成的xxx.i或xxx.ii文件,进行一系列词法分析、语法分析、语义分析及优化后,生成相应的汇编代码文件。
词法分析:利用类似于“有限状态机”的算法,将源代码程序输入到扫描机中,将其中的字符序列分割成一系列的记号。
语法分析:语法分析器对由扫描器产生的记号,进行语法分析,产生语法树。由语法分析器输出的 语法树是一种以表达式为节点的树。
语义分析:语法分析器只是完成了对表达式语法层面的分析,语义分析器则对表达式是否有意义进 行判断,其分析的语义是静态语义——在编译期能分期的语义,相对应的动态语义是在运行期才能确定 的语义。
优化:源代码级别的一个优化过程。
目标代码生成:由代码生成器将中间代码转换成目标机器代码,生成一系列的代码序列——汇编语言 表示。
目标代码优化:目标代码优化器对上述的目标机器代码进行优化:寻找合适的寻址方式、使用位移 来替代乘法运算、删除多余的指令等。
(3)汇编
将汇编代码转变成机器可以执行的指令(机器码文件)。 汇编器的汇编过程相对于编译器来说更简单,没 有复杂的语法,也没有语义,更不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译过 来,汇编过程有汇编器as完成。经汇编之后,产生目标文件(与可执行文件格式几乎一样)xxx.o(Linux 下)、xxx.obj(Window下)。
(4)链接
将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。链接分为静态链接和动态链接:
所有的预处理器命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。下面列出了所有重要的预处理器指令:
| 指令 | 描述 |
|---|---|
| #define | 定义宏 |
| #include | 包含一个源代码文件 |
| #undef | 取消已定义的宏 |
| #ifdef | 如果宏已经定义,则返回真 |
| #ifndef | 如果宏没有定义,则返回真 |
| #if | 如果给定条件为真,则编译下面代码 |
| #else | #if 的替代方案 |
| #elif | 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码 |
| #endif | 结束一个 #if……#else 条件编译块 |
| #error | 当遇到标准错误时,输出错误消息 |
| #pragma | 使用标准化方法,向编译器发布特殊的命令到编译器中 |
预定义宏
ANSI C 定义了许多宏。在编程中您可以使用这些宏,但是不能直接修改这些预定义的宏。
| 宏 | 描述 |
|---|---|
| DATE | 当前日期,一个以 “MMM DD YYYY” 格式表示的字符常量。 |
| TIME | 当前时间,一个以 “HH:MM:SS” 格式表示的字符常量。 |
| FILE | 这会包含当前文件名,一个字符串常量。 |
| LINE | 这会包含当前行号,一个十进制常量。 |
| STDC | 当编译器以 ANSI 标准编译时,则定义为 1。 |
头文件是扩展名为.h的文件,其中包含C函数的声明和宏定义,也可以多个源文件之间共享。有两种类型的头文件:程序员编写的文件,和编译器中附带的文件。
头文件的引用
使用预处理指令 #include 可以引用用户和系统头文件。它的形式有以下两种:
#include 用于引用系统头文件。它在系统目录的标准列表中搜索名为 file 的文件。在编译源代码时,您可以通过 -I 选项把目录前置在该列表前。C 语言中 include <> 与include “” 的区别
#include < > 引用的是编译器的类库路径里面的头文件。
#include " " 引用的是你程序目录的相对路径中的头文件,如果在程序目录没有找到引用的头文件则到编译器的类库路径的目录下找该头文件。
在有多个 .h 文件和多个 .c 文件的时候,往往我们会用一个 global.h 的头文件来包括所有的 .h 文件,然后在除 global.h 文件外的头文件中 包含 global.h 就可以实现所有头文件的包含,同时不会乱。方便在各个文件里面调用其他文件的函数或者变量。
#ifndef _GLOBAL_H
#define _GLOBAL_H
#include
#include
#include
#include