C语言学习

        为了更好的理解数据结构,开始重温C语言。以前学习过C语言,所以此笔记可能有些天马行空,不系统,挂个学习链接:C语言实例说明(解剖C语言) - C语言教程 - C语言网 (dotcpp.com)

1B = 1byte(字节) = 8bit 一字节存两位16进制数 

以 # 开头的指令为预处理命令: 例: #include

int main(void):  主函数,最先执行的函数,每个 c 程序必须有。int 用来指明main()函数的返回类型。括号内是函数参数,无参数填void。

注释: /* 注释 */  或者  //, 可单行也可以多行。

声明语句: 例: int   num。c 语言中,所有变量都必须在使用前定义。 变量名区分大小写。

标识符:在程序中使用的变量名、函数名、标号等统称为标识符。c规定标识符只能由字母、数字、下划线组成,第一个字符不能以数字开头,且不能与关键字重名。

数据类型:

char类型用于存储字符(比如字母或者标点),但是从技术层面讲,char类型是整数(字符的ascii码值),因为char类型存储的是整数而不是字符,%d输出ascii码,%c输出ascii码对应的字符。

因此char能作为%d输出,而int型不一定能作为%c输出。

int型数据超过[0,127]这个范围就不能用%c输出。

当一个int型数据在[0,127]这个范围内,它不一定能用%c输出。

ASCII 表上的数字 0–31 分配给了控制字符,用于控制像打印机等一些外围设备,是不可打印的 意思就是不可输出。

 关键字:不能用作用户标识符

字符输出函数:putchar():单个输出字符。

字符输入函数:getchar():接收用户输入的一个字符。与putchar一起使用。

格式化输出 printf()函数

字符可以用单引号,但是字符串只能用双引号。

*修饰符在printf()中的用法:

假如您不想事先指定字段宽度,而是希望由程序来制定该值,那么您可以在字段宽度部分使用*代替数字来达到目的,但是您也必须使用一个参数来告诉函数宽度的值是多少。具体的说,如果转换说明符为%*d,那么参数列表中应该包括一个*的值和一个d的值,来控制宽度和变量的值。该技术也可以和浮点值一起使用来指定精度和字段宽度。

print()函数的返回值:返回所打印字符的数目。输出有错误,会返回复数。

/*使用可变宽度输出字段*/
#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: %*d\n",width,number);
    printf("Then please input width and precision:\n");
    scanf("%d %d",&width,&precision);
    printf("Weight = %*.*f\n",width,precision,weight);
    return 0;
}


/*  运行结果
Please input number's width:
6
The number is:    256
Then please input width and precision:
8 3
Weight =   25.500
*/

scanf():格式化输入函数:调用形式:

scanf(”格式控制字符串“, 输入项地址列表【&变量名】);

不能显示提示字符串。

(1)格式说明符可以指定宽度,但不能指定数据的精度。

(2)输入long类型必须使用 %ld,输入double必须使用%lf或者%le。

(3)附加格式说明符 ”*“ 使对应的输入不赋给相应的变量。

*修饰符在scanf中()的用法:

*在scanf()中提供截然不同的服务,当把它放在%和说明符字母之间时,它使函数跳过相应的输入项目。

赋值操作:

有关赋值的几个术语:

数据对象:数据存储区的的术语,数据存储区能用于保存值。用于保存变量或数组的数据存储区是一个数据对象。

左值:(lvalue)标识一个特定的数据对象的名字或表达式。c用可修改的左值表示可更改的对象。

右值:(rvalue)指的是能赋给可修改的左值的量。

运算符:

1.单目运算符: 目:运算符参与运算的对象个数。

        自增++和自减--,只需要一个操作对象,且为变量,又称为增量运算符。

                前缀模式:++ i:先执行自增或自减运算,再计算表达式的值。

                 后缀模式:i ++: 先计算表达式的值,再执行自增自减运算。

        sizeof: sizeof是C语言的32个关键字之一,并非“函数”,也叫长度(求字节)运算符,其运算对象可以是任何数据类型变量

        逻辑非!:右边跟一个表达式即可

2.双目运算符:

        加法+、减法-、乘法*、除法/、求模%。

        关系运算符:大于>、小于<、大于或等于>=、小于或等于<=、是否等于==、是否不等于!=,比较结果为逻辑值。

        逻辑与&&、逻辑或||。

3. 三目运算符:

        选择运算符?:一般形式:

                表达式1?表达式2:表达式3

        解释:首先计算表达式1的值,如果表达式1成立,那么这个三目运算符整体的值就是表达式2的值,否则这个三目运算符整体的值就是表达式3的值。

表达式:

        运算符操作数组合构成的,一些表达式是多个较小的表达式的组合,这些小的表达式称为子表达式(subexpression)

语句:

        语句(statement)是构造程序的基本部分。程序(program)是一系列带有某种必须的标点语句集合。一个语句是一条完整的计算机指令。在C中,语句用结束处的一个分号标示。

         简单语句以分号结尾。

        复合语句(compound statement)代码块(block)由用花括号括起来的一个或多个语句(它们本身也可以是复合语句)组成。

条件判断:

1. if():

if(表达式) /*如果表达式成立,执行语句1否则继续判断表达式2*/ 
{ 
    //语句1 
} 
else if(表达式2) /*如果表达式成立,执行语句2否则继续判断表达式3*/ 
{ 
    //语句2 
} 
else if(表达式3) /*如果表达式成立,则执行语句3否则继续判断下一个表达式*/ 
{ 
    //语句3; 
} 
//… … 
else /*如果以上表达式都不成立 则执行语句4*/ 
{ 
    //语句4 
}

2. switch case:

        首先计算表达式的值,然后依次与常量表达式依次进行比较,若表达式的值与某常量表达式相等,则从该常量表达式处开始执行,直到switch语句结束。若所有的常量表达式的值均不等于表达式的值,则从default处开始执行。

switch(表达式) /*首先计算表达式的值*/ 
{ 
    case 常量表达式1:语句1; 
    case 常量表达式2:语句2; 
    case 常量表达式3:语句3; 
    // …… 
    case 常量表达式n:语句n; 
    default:语句n+1;
}

3. switch 与 if else 差别:

        switch中每个条件(case)是比较相等与否。而else if中的条件除了可以比较相等以外,还可以满足某个区间。

4. break语句的用法:

        仅用于跳出switch结构循环结构,用于提前结束switch结构或循环。

#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;
}

5 while 循环:        

        1.while循环:

          while语句创建一个循环,该循环在判断表达式为假(或0)之前重复执行。while语句是一个入口条件(entry-condition)循环,在进行一次循环之前决定是否要执行循环。因此有可能一次也不执行。循环的语句部分可以是一个简单语句或一个复合语句

        2.do while 循环:

        do while语句创建一个循环,它在判断表达式为假(或0)之前重复执行。do while语句是一个退出条件循环,在执行一次循环之后才决定是否要再次执行循环,因此循环至少要被执行一次。循环的语句部分可以是一个简单语句或一个复合语句

do 
{ 
    循环体语句 
}while(表达式);

        上述两种循环体完全可以用for循环替代。由于循环的三大条件:初始值增量循环条件都写在一起,避免了while循环do while循环容易遗忘初始化、增量的情况,并且书写起来更简洁、直观,控制能力更强,请大家理解并学会for循环,并且通过for循环理解循环的必要条件,更深刻的理解循环的本质。

        2.循环的嵌套 :

        可以看到for循环里的循环体亦然是一个循环体(while、do-while循环也如此,也可以三种循环混合使用),这种情况下,内部的循环全部执行完(即j从0执行到10),外部的循环才会执行一次(即i会增加1),这就是循环嵌套的最大的规律和理解关键,外部循环执行一次,内部循环执行一轮!

6.continue:

        需要注意的是,continue语句并不能像break那样可以在switch循环中都可以使用,continue只能在循环中使用!这点大家务必牢记!

7.阶段答疑:

1、问:我想判断一个数字是否在一个区间里,比如if(90<a<100)可以吗?

答: 不可以的! 关系运算符无论是大于小于还是不等于等等,都属于双目运算符,即参与运算的对象是两个。两个条件需要两个表达式分开表达,用逻辑与链接,即90<a && a<100。

学生追问:那为什么这样写编译器并且没有报错呢?

黄老师答:90<a<100这个表达式的意思对于编译器看来是看成一个复合表达式,即(90<a)<100,语法上来讲是合法的,编译器会算计算90<a的值是真是假,即1或0,再于100进行比较,语法上是完全解释的通的,但if里这个表达式的值基本可以肯定恒成立了,因为无论90<a的结果如何,不是1就是0,但一定小于100!

2、我想判断一个变量是不是一个数字,比如判断a是否是99写成if(a=99)不对吗?

黄老师答:在C语言里,判断两个值是否相等要用到算数运算符==! 一个等号=意思为赋值,两者意义完全不同,切勿混淆!

学生追问:

那这样写不可以,为什么编译不报错呢?

黄老师答:答案是在if的括号里,a=90这个表达式被编译器看来是一个值,也就是90,即与if(90)没有什么区别,仅多一个赋值(把90给a)的副作用。所以完全合法!只不过现在恒为真了!

4.比较两个字符串怎么写?if(str1==str2) 对吗?

黄老师答:不可以!比较字符串核心是比较两个字符串的内容,可以参考使用strcmp函数。但不可以用数组名直接比较!因为数组名表示的是数组首元素的地址!

学生追问:那就是说这么比较不是在比较两个数组的内容?

黄老师答:对,而是比较两个地址,是没有用的!

EOF:stdio头文件中定义的宏,值为 -1。

为什么这样做呢? stdio.h作为一个标准输入输出的头文件,自然会定义很多标准的东西,那么EOF正是因此而生,且听笔者慢慢道来:

在我们进行包括scanf等的输入函数使用时,其实用户在cmd中的输入实际是存放于缓冲区当中,当用户键入回车那一瞬间,之前输入的数据才会被存进去,而这里无论是单个字符还是字符串,我们都知道scanf的返回值呢是表示成功接受到的对象的个数,那这里如果遇到特殊情况,比如缓冲区文件流满等问题,那么scanf将如何处理呢?答案是返回-1 ! 这里不光是scanf,返回值为个数的函数,遇到文件流满大多都会返回-1,所以这个-1用的比较多,那么stdio.h就索性专门定义一个宏来表示,取End Of File(文件末尾的意思)的前三个字母即组成EOF,所以也就有了 #define EOF (-1) 这样的话!

long long 的用法:

long long 的级别高于 long ,long 的级别高于 int ,int 的级别高于 short ,short 的级别高于 char 。(另外有 _Bool 永远是最低级别)。级别高的整数类型的宽度大于等于级别较低的整数类型。

// C语言筛选n以内的素数
#include<stdio.h>
int main()
{
	int n;
	scanf("%d", &n);
	printf("%d\n", 2);
	for(int i = 3; i <= n; i ++)
	{
	    for(int j = 2; j <= i; j ++)
	    {
	        if(i % j == 0)
	        {
    	        if(i == j)
    	        {
        	        printf("%d\n", i);
    	        }
    	        break;
	        }
	    }
	}
}

5、函数

函数的定义:

返回值类型 函数名(形参表说明) /*函数首部*/
{
    说明语句 /*函数体*/
    执行语句
}

(1)“返回值类型”是指函数返回值的类型。函数返回值不能是数组,也不能是函数,除此之外任何合法的数据类型都可以是函数的类型,如:int,long,float,char等。函数类型可以省略,当不指明函数类型时,系统默认的是整型。

(2)函数名是用户自定义的标识符,在C语言函数定义中不可省略,须符合C语言对标识符的规范,用于标识函数,并用该标识符调用函数。另外函数名本身也有值,它代表了该函数的入口地址,使用指针调用函数时,将用到此功能。
(3)形参又称为“形式参数”。形参表是用逗号分隔的一组变量说明,可以是任何一种合法的C语言类型:包括形参的类型和形参的标识符,其作用是指出每一个形参的类型和形参的名称,当调用函数时,接收来自主调函数的数据,确定各参数的值。

(4)形参传递的值:如果形参列表的参数是基本类型变量或者结构体类型变量,传递的是实参值的副本,将实参值复制一份给形参,如果形参列表的参数是复杂类型(如数组指针等),传递的是实参的地址值,将实参地址值传递给形参,形参和实参共用同一地址空间。
(5)用{ }括起来的部分是函数的主体,称为函数体。函数体是一段程序,确定该函数应完成的规定的运算,应执行的规定的动作,集中体现了函数的功能。函数内部应有自己的说明语句执行语句,但函数内定义的变量不可以与形参同名。花括号{ }是不可以省略的。

形参与实参的区别:

                                      形参

                实参
形参变量只有在被调用时才分配内存单元,在调用结束时,即刻释放所分配的内存单元。因此,形参只有在函数内部有效。函数调用结束返回主调函数后则不能再使用该形参变量实参定义后就会分配内存
形参出现在函数定义中,在整个函数体内都可以使用, 离开该函数则不能使用实参出现在主调函数中,进入被调函数后,实参变量也不能使用
形参没有确定的值实参在值传递给形参的时候,必须要有确定的数值

C语言函数的调用

主调函数使用被调函数的功能,称为函数调用。在C语言中,只有在函数调用时,函数体中定义的功能才会被执行。C语言中,函数调用的一般形式为:

函数名(类型 形参,类型 形参...);

对无参函数调用时则无实际参数表。实际参数表中的参数可以是常数、变量或其他构造类型数据及表达式,各实参之间用逗号分隔。

调用方式:

(1)函数表达式:函数作为表达式中的一项出现在表达式中,以函数返回值参与表达式的运算。

(2)函数语句:函数调用的一般形式加上分号即构成函数语句。例:priintf("%d", a);

(3)函数实参:函数作为另一个函数调用的实际参数出现。这种情况是把该函数的返回值作为实参进行传送,因此要求该函数必须是有返回值的.

printf("%d",max(x,y));/*把max调用的返回值作为printf函数的实参*/

在主调函数中调用某函数之前应对该被调函数进行声明,在主调函数中对被调函数进行声明的目的是使编译系统知道被调函数返回值的类型,以便在主调函数中按此种类型对返回值进行相应的处理。其一般形式为:

类型说明符 被调函数名(类型 形参,类型 形参...);

需要注意的是,函数的声明函数的定义有本质上的不同,主要区别在以下两个方面:

(1)函数的定义是编写一段程序,应有函数的具体功能语句——函数体,而函数的声明仅是向编译系统的一个说明,不含具体的执行动作。
(2)在程序中,函数的定义只能有一次,而函数的声明可以有多次。

(3)在调用时,如果调用的函数在main 函数之后,要先在main函数之前,对该函数进行声明,然后才可以正常调用。

函数体内是实现某一特定功能的算法:算法就是解决实际问题的方法和步骤。

不同类型函数的参数传递过程

1.基本数据类型变量做函数形参

int fun(int a, int b)
{    
    int c;
    c = a + b;
    return c;
}

void main()
{
    int a, b, d;
    a = 3; b = 5;
    d = fun(a, b);  // 传递的是实参值副本
}

2.数组类型做形式参数

int fun(int a[], int n)
{
    int i, sum=0;
    for(i=0; i<n; i++)
    {    
        sum += a[i];
    }
    return sum;
}

void main()
{
    int d, a[5] = {1};
    d = fun(a, 5);  // 传递的是地址值,数组a的首地址。
}

 3.指针做形式参数

int fun(int *a, int n)
{
    int i, sum=0;
    for(i=0; i<n; i++)
    {    
        sum += a[i];
    }
    return sum;
}

void main()
{
    int d, a[5] = {1};
    int *t;
    t = a;
    d = fun(a, 5);  // 传递的是地址值,数组a的首地址。fun(t, 5)也可以。
}

C语言变量的存储类型***

在C语言中,变量是对程序中数据所占内存空间的一种抽象定义,定义变量时,用户定义变量的名、变量的类型,这些都是变量的操作属性。不仅可以通过变量名访问该变量,系统还通过该标识符确定变量在内存中的位置。

计算机的存储单元两类:内存和cpu寄存器。C语言中定义了4种存储属性,即自动变量(auto)、外部变量(extern)、静态变量(static)和寄存器变量(register),它关系到变量在内存中的存放位置,由此决定了变量的保留时间和变量的作用范围。

生存期:变量的保留时间。

(1)静态存储是指变量存储在内存的静态存储区,在编译时就分配了存储空间,在整个程序的运行期间,该变量占有固定的存储单元,程序结束后,这部分空间才释放,变量的值在整个程序中始终存在。

(2)动态存储是指变量存储在内存的动态存储区,在程序的运行过程中,只有当变量所在的函数被调用时,编译系统才临时为该变量分配一段内存单元,函数调用结束,该变量空间释放,变量的值只在函数调用期存在。

作用域: 变量的作用范围,有全局变量和局部变量。

局部变量是在函数或者复合语句内定义的变量,仅在函数和复合语句内有效,编译时,系统不为其分配内存单元,而是在运行过程中根据需要临时分配内存,调用结束空间即释放。

全局变量是在函数之外定义的变量,其作用范围为从定义处开始到本文件结束,编译时,编译系统为其分配固定的内存单元,在程序运行的自始至终都占用固定单元。

自动变量:

函数中的局部变量,如不专门声明为static存储类别,都是动态地分配存储空间的,数据存储在动态存储区中,这些变量称为自动变量。用关键字auto进行存储类别声明。(一般不用声明)。

extern外部变量(全局变量)

函数外部定义的,作用域从定义处开始,到本程序文件末尾。

如果在定义点之前的函数想引用该外部变量,则应该在引用之前用关键字extern对该变量进行“外部变量声明”,表示该变量是一个已经定义的外部变量。有了此声明,就可以从“声明”处起,合法地使用该外部变量。

C语言static静态变量详解

通过用static类型声明后的变量,其变量的内存空间位于内存的全局静态区,仅会初始化一次,这是一个非常重要的特性。(每次值都是接着上一次函数调用的值继续运算)。

C语言register寄存器变量详解(寄存器中的数据是有生命周期的)

注意:

        (1)只有局部自动变量和形式参数可以作为寄存器变量。
        (2)一个计算机系统中的寄存器数目有限,不能定义任意多个寄存器变量。
        (3)不能使用取地址运算符“&”求寄存器变量的地址。

目前定义寄存器变量,编译器很有可能把其忽略掉。

***C语言数组的定义和使用

一维数组

1. 定义方式:

类型说明符 数组名 [常量表达式];

只定义数组时,数组里的值都是未初始化过的,我们也可以初始化赋值,部分赋值时,int型未被赋值元素为零,浮点型为小数类型,字符类型为'\0'。

二维数组

类型说明符 数组名 [行数][列数];

 和一维数组同,在内存中线性排布。

字符串数组

用来存放字符的数组称为字符数组。数组名表示该数组的首地址,这为处理字符串中个别字符和引用整个字符串提供了极大的方便。

定义赋值与前面相同,不同的是,会为字符数组末尾自动添加字符串结束符:‘\0’。输出时,遇到结束符便停止继续输出。

可采用字符串常量赋值:

char a[]={"china"};

C语言字符串常见处理操作: 

strcpy(): 拷贝一个字符串到另一个字符串数组。

        头文件: <string.h>

函数原型: char *strcpy(char *destin, const char *source);

strcat(): 将一个字符串拼接在目标字符串的后面,必须保证目标字符串有足够的空间容纳待拼接的字符串,且只能是字符数组,而不能是字符串指针指向的字符串,因为字符串指针指向的是字符串常量, 常量不能被修改。

strcmp(): 字符串比较,比较两个字符串大小,区分大小写。

函数原型:  int strcmp(const char *str1,const char *str2);

功 能:   比较两个字符串的大小,区分大小写(strcmpi() 不区分大小写)

参 数: str1和str2为要比较的字符串

返回值 str1 > str2 , 返回 1;

                str1 < str2 , 返回 -1;

                str1 == str2 , 返回 0;

函数名:  strchr

头文件:<string.h>

函数原型:  char *strchr(const char *str, char c);

功能: 查找字符串中第一个出现的指定字符的位置

参数: char *str  为要查找的目标字符串;

           char c    为要查找的字符;  

返回值: 成功  返回字符第一次出现的位置;失败  返回NULL;

***指针

先明确地址的概念:地址内存(计算机虚拟的逻辑内存空间)中是唯一标识某一点的编号。

指针变量: 内存比作尺子的话,指针是尺子上边的游标,可以左右移动,某一时刻指向某一地方。任何类型的指针变量都占四个字节。是用来存放内存地址的变量。

定义形式:类型说明符 *变量名。

说明:所有指针变量都在内存中只占四个字节(4B),且每个指针在使用时都要为其赋值,指定一个地址,而且指向该地址的数据类型要与指针的数据类型一致

int *p1, *p2;
p2 = p1;  // 作用,改变p2的指向的地址,让其指向p1所指向的地址(不改变地址中的存储的数据),也就是说二者现在的值是相等的。


// 使用指针变量的三个步骤:
// 1.定义一个指针变量
int *p;
int a;
// 2.初始化指针变量赋值,无具体地址赋值为NULL,但不能不赋值。
p = &a;
// 操作指针变量。
*P = 80;  // 间接存取方式, 直接存取方式:使用变量标识符直接存取内存单元的数据。

此外:*可作为运算符获取地址上对应的值。

空指针

        在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值是一个良好的编程习惯。赋为 NULL 值的指针被称为指针。NULL 指针是一个定义在标准库中的值为零的常量,是一个无效指针。

void *类型的指针

        表明这是一个有效的指针,指向内存中的某一空间,但是并没有指定类型(指针指向的数据类型未知),使用时要指向一个确定的类型(可指向任意类型),即将这种类型的指针强制转化为某种数据类型。

举例:maolloc函数申请内存空间,返回为void类型。

// 语法结构
void *malloc(unsigned int num_bytes);  
// malloc()函数执行成功并不知道所申请的内存空间存放什么类型的数据,
// 返回为void *,使用时,需要将函数的返回类型强制转换为特定的指针类型。
int *p;
p = (int *)malloc(size of(int));
// malloc()函数返回值强制转换成int型指针,并赋值给指针变量p;

指针变量作为函数参数

#include<stdio.h>
void swap(int *a, int *b)  // 传入的是对应类型变量的地址, 交换的是指针指向变量的地址对应的值
{
    int t;
    t = *a;
    *a = *b;
    *b = t;
}
int main()
{  
    int m=6, n=7;
    swap(&m, &n);
    printf("%d %d\n", m, n);
}

C语言数组与指针的区别与联系

一维数组的数组名表示首元素的地址。

二维数组的数组名表示为首行的行地址,每行都可看做一个一维数组,一维数组名表示该行首列元素的地址。a[2][3], a, a+1为行地址,a[0], a[0] + 1为列地址。

数组可以定义两种类型的指针:

        (1)指向数组元素的指针,指针变量存放是数组中元素的地址。

        (2)指向整个一维数组的指针变量,指针存的是整个一维数组的地址(行地址)。

 可以用指针的方式访问数组中的元素。

指向数组元素的指针变量的定义和赋值方法与指针变量相同。

int a[10];
int *p;
p = &a[0];  // 把a[0]元素的地址赋给指针变量p。
p = a + 4;
*p = 80;  // a[4]赋值80,等价*(a + 4) = 80


// c语言数组名为元素的首地址, 故有:
p = a;  // 等价于p = &a[0];
int *p = a;  // 等价于 *p = &a[0];

 对于指向首地址的指针p,p+i(或a+i)就是数组元素a[i]的地址,*(p+i)( 或*(a+i) )就是a[i]的值。

 引入指针变量后,就可以用以下两种方法来访问数组元素
(1)下标法,即用a[i]形式访问数组元素,在前面介绍数组时都是采用这种方法。
(2)指针法,即采用*(a+i)或*(p+i)形式,用间接访问的方法来访问数组元素,其中a是数组名,p是指向数组的指针变量,其初值p=a。

指针可以通过++或--并修改自身值的方式移动,但是数组本身值不可以被更改,如数组名a为常量,不可修改。

*p++ == *(p ++);  * and ++ 都是单目运算符,优先级相同,结合自右向左。 ++(*p)对p中的变量进行前加运算。

定义指向二维数组元素的指针变量

int *p;
int a[2][3];
p = &a[0][0]; // 指针指向了二维数组a[0][0]元素

// 赋值
int *p, a[2][3];
p = &a[1][2]; // 先获取元素,在获取元素的地址。

p = a[0] + 0; // 等价于&a[0][0]
p = a[1] + 1; // 等价于&a[1][1]

// 行地址转换为列地址, 在二维数组的行地址前加一个指针运算符 *

// a 和 a + 1 为行地址,则*(a), *(a+1)为列地址

// 列地址转换为行地址, 在二维数组的列地址前加一个指针运算符 *
// &a[0]等价于 a, &a[1] 等价于 a+1。
// 行地址是比列地址更高一级的地址,相当于二级指针
 
#include<stdio.h>
int main()
{  
    int a[2][3] = {{9, 8, 7}, {6, 5, 4}};
    int *p;
    *p = a[0][0];
    for(int i=0; i<2; i++)
    {
        for(int j=0; j<3; j++)
        {
            printf("%d ", *(*(a+i)+j));  // (a+i) 是行地址,*(a+i)将其转化为列地址。
        }
        printf("\n");
    }
    
}

 指向一维数组的指针变量

又称为行指针

格式: 数据类型 (* 指针变量)[m];  字符指针数组:*变量[m],别弄混

int (*p)[4];  // 小括号不可以省略
// 为行指针赋值,不能再使用单个元素的地址,要使用整个一维数组的地址。

int (*p)[3];
int a[2][3];
p = a;  // p 指向二维数组的首行,一定不能使用列地址为行指针赋值。

 指向一维数组的指针变量经常与二维数组结合使用,开始需要使用行地址为其赋值,操作时转换为指向元素的列指针,才能操作数组元素值。

#include<stdio.h>
int main()
{  
    int a[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
    int (*p)[4], i, j;
    p = a;
    for(i=0; i<3; i++)
    {
        for(j=0; j<4; j++)
        {
            printf("%d\t", *(*(p+i)+j));
        }
        printf("\n");
    }
    
}

C语言字符串与指针的区别与联系

字符指针也可以指向一个字符串,可以用字符串常量对字符指针进行初始化,字符指针指向一个字符串常量的首地址

字符串指针和字符串数组两种方式都可以访问字符串,但它们有着本质的区别:字符指针str是个变量,可以改变str使它指向不同的字符串,但不能改变str所指向的字符串常量的值。而string是一个数组,可以改变数组中保存的内容。应注意字符串指针和字符串数组的区别。

指针数组

        引入目的:管理指针变量

        定义:

char *str[3];
char name[][8] = {"china", "england", "france"}; 
// 相当于三行八列的数组,每行字符串长度不超过8
str[0] = name[0];  // str[0] point "china"
str[1] = name[1];  // str[1] point "england"
str[2] = name[2];  // same principle

 字符型指针数组广泛用于处理字符串。

多级指针

C语言既可以定义二级指针,也可以定义三级指针。

二级指针:指针变量指向另一个指针类型变量,也就是指向指针的指针变量。二维数组的行指针就是二级指针。

定义:

int **p;  // *(*p)

int a = 10;
int *p, **q;
p = &a;  // get variable a adresss to pointer p
q = &p;  // get pointer p adress to sencond pointer q
int k = **q;  // get variable equal 10
printf("%d", k);

 指针与函数

        指针与函数的关系两种:指向函数的指针和指向指针的函数。

指向函数的指针

#include<stdio.h>

int findMax(int a, int b)
{
    int z;
    if(a > b) z = a;
    else z = b;
    return z;
}

int main()
{
    int a=6, b=8;
    int z = 0;
    int (*p_max)(int, int);  // 数据类型 (*指针变量名)(函数参数列表(可以省略))
    p_max = findMax;  // 指针要先赋值才能用,指针指向 fundMax()函数
    z = p_max(a, b);
    printf("%d", z);
}

 返回值为指针的函数

// 返回值为指针的函数
int z;  // 返回的指针指向的地址要为全局变量。
int *fun(int a, int b, int c)
{
    int i;
    for(i=1; ; i ++)
    {
        if(i%a == 0 && i%b == 0 && i%c == 0)
        {
            break;
        }
    }
    z=i;
    return &z;
}

int main()
{
    int a=6, b=8, c=12, *p;
    p = fun(a, b, c);  // 接收指针函数返回的地址
    printf("%d", *p);
}

用指针作为函数返回值时需要注意的一点是,函数运行结束后会销毁在它内部定义的所有局部数据,包括局部变量、局部数组和形式参数,函数返回的指针请尽量不要指向这些数据,C语言没有任何机制来保证这些数据会一直有效,它们在后续使用过程中可能会引发运行时错误。 

指针在程序开发中的应用

        指针的一大优势,定义的指针变量能够直接保存硬件的内存地址,从而直接访问内存中的数据,提高了数据的存取速度(C语言程序的速度快的原因),应用非常广泛。

应用1:利用行指针将二维数组的值保存到一维数组中。

#include<stdio.h>

int twoToline(int (*p)[4], int b[])  // define a array pointer
{
    int i, j;
    int count = 0;
    for(i=0; i<4; i++)
    {
        for(j=0; j<3; j++)
        {
            b[count] = *(*(p+j)+i);
            count ++;
        }
    }
    return count;
}

int main()
{
    int a[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};
    int b[100] = {0};  // 要赋初值,否则内部的变量具有随机值,导致输出不对
    int count = 0;
    count = twoToline(a, b);
    for(int i = 0; i<12; i++)
    {
        printf("%d ", b[i]);
    }
    printf("\n%d", count);

}

 应用2: 删除字符串中指定元素

int main()
{
    char s[] = {"abcdefg"};
    char c = 'd';
    deleteCharFromString(s, c);
    printf("%s\n", s);

}

void deleteCharFromString(char *a, char c)
{
    int i, j;
    char *s;
    s = a;
    for(int i=j=0; s[i] != '\0'; i++)
    {
        if(s[i] != c)
        {
            s[j++] = s[i];
        }
    }
    s[j] = '\0';  // 不加结束符,旧字符串后边的字符还会继续输出。
}

结构体

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

结构体的定义说明了它的组成成员,以及每个成员的数据类型。

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

结构的定义说明了变量在结构中的存在格式,要使用该结构就必须说明结构类型的变量。结构变量说明的一般形式如下:

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

程序中使用结构中成员方法:  结构变量名 . 成员名称。

结构体初始化:

struct 结构类型名 结构变量 = { 初始化数据 1, ...... 初始化数据 n };

结构体数组:(类似于python的字典)。

结构体数组是一个数组,其数组的每一个元素都是结构体类型。经常用结构体数组来表示具有相同数据结构的一个群体,如一个班的学生档案,一个车间职工的工资表等。

结构体指针:指向结构体的指针。当一个指针用来指向一个结构体变量时,称之为结构体指针变量。结构体指针变量中的值是所指向的结构变量的首地址,通过结构指针即可访问该结构变量。

定义形式:

struct 结构类型名 *结构指针变量名

定义之后,就可以像之前学过的指针一样,将结构体类型的变量赋值给这个指针即可,后面就可以用这个指针间接的访问结构体了,需要注意的是,不用于结构体变量用点(.)来访问成员的方法,结构体指针是通过箭头(->)来访问的。

// 通过循环,循环三次来输出全部A数组中的成员内容
#include<stdio.h>
#include<string.h>
struct adress
{
    char name[30];
    char adress[20];
    unsigned long tel;
    unsigned long zip;
}student[3] = 
{
    {"tom", "street 1", 12343434, 123},
    {"tony", "street 2", 123134567, 234},
    {"jerry", "street 3", 12345456, 345}
};
// struct adress student[3] = ... 也行。
int main()
{
    struct adress *p;
    p = &student[0];
    for(int i=0; i<3; i++)
    {
        printf("%s %s %u %u\n", (p+i)->name, (p+i)->adress, (p+i)->tel, (p+i)->zip);
    }
}

 用结构体变量做函数参数

函数调用时实参应该是结构体变量,且是参与形参之间为单项值传递,即将实参中结构体变量成员按一对一的方式复制传递给形参成员。

 用结构体指针变量做函数参数

函数调用时,实参应是结构体类型的地址值,参数传递时,将实参的地址值,传递给了形参结构体的指针变量,形参的结构体指针指向了实参的内存单元。

union共用体的定义和使用

在C语言中,允许几种不同类型的变量存放到同一段内存单元中,也就是使用覆盖技术,几个变量互相覆盖。这种几个不同的变量共同占用一段内存的结构,被称为共用体类型结构,简称共用体

定义形式:

union 共用体名 
{ 
    数据类型 成员名 1; 
    数据类型 成员名 2; 
    ...... 
    数据类型 成员名 n; 
}变量名表列;

只有先定义了共用体变量,才能在后续的程序中引用它。不能直接引用共用体变量,而只能引用共用体变量中的成员。引用方法如下:

共用体变量名.成员名

共用体类型数据特点: 

(1)同一个内存段可以用来存放几种不同类型的成员,但是在每一瞬间只能存放其中的一种,而不是同时存放几种。换句话说,每一瞬间只有一个成员起作用,其他的成员不起作用,即不是同时都存在和起作用的。
(2)共用体变量中起作用的成员是最后一次存放的成员,在存入一个新成员后,原有成员就失去作用。共用体变量的地址和它的各成员的地址都是同一地址。

(3)不能对共用体变量名赋值,也不能企图引用变量名来得到一个值,并且,不能在定义共用体变量时对它进行初始化。
(4)不能把共用体变量作为函数参数,也不能是函数返回共用体变量,但可以使用指向共用体变量的指针。共用体类型可以出现在结构体类型的定义中,也可以定义共用体数组。反之,结构体也可以出现在共用体类型的定义中,数组也可以作为共用体的成员。

C语言typedef用法详解

在C语言中,除系统定义的标准类型和用户自定义的结构体、共用体等类型之外,还可以使用类型说明语句typedef定义新的类型来代替已有的类型。

定义类型:

typedef 已定义的类型 新的类型;

// 为基本数据类型命名
typedef double DB;
DB f = 5.2;  //等价于double f = 5.2

// 为指针类型重命名
typedef int *IP;
IP p; // equal int *p;  IP *p; 等价于int **p;

// 为结构体类型重命名
typedef struct student
{
    char ID[20];
    char name[20];
    char grade[20];
}STUDENT;

STUDENT stu;  // 等价于 struct student stu;

C语言enum枚举类型详解

形式:

enum 枚举名 {枚举元素1,枚举元素2,……};
// 例:
enum Week{MON, TUE, WED, THU, FRI, SAT, SUN};  // 七个值分别为(0, 1, 2, 3, 4, 5, 6)

枚举类型的特点默认是从0开始递增,若想更改,可以将其中某个类型赋值,后面的值将在此基础之上递增。

C语言宏定义define的用法

预处理命令:

改变程序设计环境,提高编程效率,不能编译。

预处理功能共三种:宏定义、文件包含、条件编译。

宏定义在C语言源程序中允许用一个标识符来表示一个字符串,称为“宏”,被定义为“宏”的标识符称为“宏名”。在编译预处理时,对程序中所有出现的宏名,都用宏定义中的字符串去代换,这称为“宏代换”或“宏展开”。宏定义是由源程序中的宏定义命令完成的,宏代换是由预处理程序自动完成的。在C语言中,宏分为有参数和无参数两种。无参宏的宏名后不带参数,其定义的一般形式为:

#define 标识符 字符串;

#define M (y*y+3*y);  //M替代表达式。


//带参宏定义。
#define 宏名(形参表) 字符串;  // 字符串含有各个形参。

// 带参宏调用:宏名(实参表)。
#define M(y) y*y+3*y;  // 宏定义
....
k=M(5);  // 宏调用, 实参5替代形参y。

其中“#”表示这是一条预处理命令(在C语言中凡是以“#”开头的均为预处理命令)“define”为宏定义命令,“标识符”为所定义的宏名,“字符串”可以是常数、表达式、格式串等。符号常量的定义就是一种无参宏定义。

实例:

#include <stdio.h>
#define MAX(a,b) (a>b)?a:b
/*带参数的宏定义*/
void main()
{
    int x,y,max;
    printf("input two numbers: ");
    scanf("%d %d",&x,&y);
    max=MAX(x,y);
    printf("max=%d\n",max);
    /*宏调用*/
}
// 宏替换相当于实现了一个函数调用的功能,而事实上,与函数调用相比,宏调用更能提高C程序的执行效率。

 文件包含

#include<stdio.h>
#define Remainder(x, y) x % y  // 宏定义不带分号,可直接作为参数传入函数中。
int main()
{
	int a, b, c;
	scanf("%d %d", &a, &b);
	printf("%d", Remainder(a, b));
}

形式:

#include "文件名"
#include <文件名>

功能:指定文件插入该命令行位置取代该命令行,把指定文件和当前的源程序文件连成一个源文件。(相当于python的import, 导入库函数)。

说明:

(1)包含命令中的文件名可以用双引号引起来,也可以用尖括号引起来。

#include "stdio.h"
#include <stdio.h>

两种形式是有区别的:使用尖括号表示在包含文件目录中去查找(包含目录是由系统的环境变量进行设置的,一般为系统头文件的默认存放目录,比如Linux系统在/usr/include目录下);使用双引号则表示首先在当前的源文件目录中查找,若未找到才到包含目录中去查找。

(2)一个include命令只能指定一个被包含文件,若有多个文件要包含,则需用多个include命令。

(3)文件包含允许嵌套,即在一个被包含的文件中又可以包含另一个文件。

条件编译 

预处理程序提供了条件编译的功能,可以按不同的条件去编译不同的程序部分,因而产生不同的目标代码文件,这对于程序的移植和调试是很有用的。条件编译可分为三种形式。

/*第一种形式*/
#ifdef 标识符
程序段 1
#else
程序段 2
#endif

// 它的功能是如果标识符已被#define命令定义过则对程序段1进行编译;否则对程序段2进行编译。
如果没有程序段2(为空),本格式中的#else可以没有。


#ifdef 标识符
程序段
#endif


/*第二种形式*/
#ifndef 标识符
程序段 1
#else
程序段 2
#endif

// 如果标识符未被#define命令定义过则对程序段1进行编译,否则对程序段2进行编译。(与第一种形式相反)


/*第二种形式*/
#if 常量表达式
程序段 1
#else
程序段 2
#endif

// 它的功能是如果常量表达式的值为真(非0),则对程序段1进行编译,否则对程序段2进行编译。

作用:因此可以使程序在不同的条件下完成不同的功能。

1.#error:

#error指令强制编译程序停止编译,它主要用于程序调试

#error error-message

注意:宏串error-message不用双引号引起来。遇到#error指令时,错误信息被显示,可能同时还显示编译程序作者预先定义的其他内容。

2. #line
#line指令改变__LINE__和__FILE__的内容。__LINE__和__FILE__都是编译程序中预定义的标识符。__FILE__的内容是当前被编译源文件的文件名。

#line标识符__LINE__的内容是当前被编译代码行的行号,其一般形式是:

1

#line number "filename"


其中,number是正整数并变成__LINE__的新值;可选的“filename”是合法文件标识符并变成__FILE__的新值。#line主要用于调试和特殊应用。

3. #pragma
#pragma是编译程序实现时定义的指令,它允许由此向编译程序传入各种指令。例如:一个编译程序可能具有支持跟踪程序执行的选项,此时可以用#pragma语句选择该功能,编译程序忽略其不支持的#pragma选项。使用#pragma预处理命令可提高C源程序对编译程序的可移植性。

c语言的宏定义陷阱:加减乘除的优先级问题。

C语言位运算符表

与其他运算符不同,C语言中位运算,顾名思义,是以数值的二进制位为单位进行操作的,包含<<(左移)、>>(右移)、~(按位取反)、&(按位与)、|(按位或)、^(按位异或)共六种运算符。

左移运算符<<向左(即高位)移位,右侧补0
右移运算符>>向右(即低位)移位,左侧补0
按位取反~如名,即0变1,1变0
按位与&相对应的两个位都为1则为1,反之为0
按位或|相对应的两个位至少有一个为1即为1,反之为0
按位异或^相对应的两个位相同为0,相异(不同)为1

移位运算符的作用:

1. 左移N位的本质是乘以2的N次方。

2. 右移N位的本质是除以2的N次方。(取整数部分)

按位与运算符的作用(&)

1. 清零:

我们可以对某一个数与0进行按位与运算,由于两个位都为1才为1,因此最终全部位都变为0,起到清零的作用

2. 取指定位:

如某些存储场景下,“第1~3位表示xxxx“”,我们需要取出1~3位,则可以让原数值与数字7进行按位与运算,得到的结果即是原数值的1~3位的值。

3. 判断奇偶:

可以发现,数字的奇偶取决于二进制位的最低一位是1还是0,因此只需要与1按位与运算,判断是1是0即可得知奇偶。

C语言|按位或运算符详解

作用:

对一个数字的指定位置为1,如“某个数字的第七位”表示开关,原先是0,需要改为1的状态,即可以将这个数字与64按位或,即可得到第七位变为1,其余位的值依旧不变。

C语言^按位异或运算符详解

原理:不同为1,相同为0。

异或运算符的作用:

1. 指定位数的翻转:

如想对某个数字的低4位进行翻转,则可以将这个数字与15(二进制为00001111)进行按位异或运算,既可以将原数字的低四位进行翻转,即高四位不变,低四位0变1,1变0。

2. 与0异或还是原值:

大家可以自行实验,一个数字与0进行异或,结果还是原值。

3. 交换两个数字:

除了之前我们学习交换两个数字需要第三个变量做中介之外,如今可以通过异或运算进行:

4. 几个c语言难题:

几个C语言的难题 | C语言研究中心 - C语言网 (dotcpp.com)

c语言运算符优先级 

 一些运算符记号在表 2 中出现了两次。例如,自增运算符 ++ 和自减运算符 --,在作后缀运算符(如表达式 x++)时,较其用作前缀运算符(如表达式 ++x)时,具有较高的优先级。

+、-、* 和 & 运算符记号不但可以当作一元运算符(unary operator,只需要一个操作数),也可以当作二元运算符(binary operator,需要两个操作数)。例如,* 只有一个操作数的时候,就是间接运算符(indirection operator),而有两个操作数的时候,就是乘号。

在这些例子中,一元运算符比二元运算符具有更高的优先级。例如,表达式 *ptrl**ptr2 等同于表达式(*ptrl)*(*ptr2)。  -----摘自C语言中文网

C语言文件处理

文件的存储方式:

        1. ASCII 码存储:数据以字符的ASCII码形式存放,磁盘上的每个字节单元中只存放一个字符,该类文件为文本文件。优点:便于对文件数据进行逐个字符操作,操作系统或者文本编辑器可以直接阅读。缺点:占用磁盘空间多,读写文件时,需要把ASCII码转换成二进制,读写速度慢。

        2. 二进制存储:数据的补码直接存储在磁盘文件中。该类文件为二进制文件,优点;读写文件不需要数据转换,读写速度快,缺点:不可直接阅读。

文件的存储过程:

ANSI C 标准:采用缓冲文件系统对文本文件和二进制文件进行读/写处理,系统自动为每个正在使用的文件开辟一片缓冲区。读数据:文件取到数据,先放进缓冲区,然后将缓冲区数据逐个送入程序变量,写数据:程序数据先送到缓冲区,缓冲满后,写进文件。

读写方式:顺序读写,和随机读写。文本文件可顺序/随机;二进制文件随机。

文件顺序读写

        C语言利用系统库 [stdio.h] 函数实现文件读写。

                1.打开磁盘文件;建立程序与磁盘文件之间的联系。

                2.读写文件数据。

                3.关闭磁盘文件;切断程序与磁盘文件间的联系,清空缓冲区。

打开文件

        fopen()函数:

        文件打开方式:

注意:fopen() 返回值为FILE (库中typedef定义的结构体类型名)类型的文件指针,操作多个文件需要建立多个文件指针。在文件读写操作方式后面分别加入“b”格式均表示对二进制文件进行操作,含义与文本文件同。 

“r+”与“w+”的区别: 打开不存在的文件时,前者不会自动创建文件,而是返回文件打开失败,后者会自动创建一个新文件。文件存在时,前者不会清空文件原内容,而是将新数据覆盖原数据,即可先读再写。后者会自动清空原文件,清空后再写新数据,不可先读,只能先写后读。

#include<stdio.h>
FILE *fopen(const char *filename, const char *mode);  // filename: file name with file path.  mode: the way open the file. read write and so on.


//demo
FILE *fp;
if(fp=fopen("test.txt", "r") == NULL)
{
    exit(0);
}
// 文件打开成功,得到一个指向文件的指针变量fp,打开失败直接退出程序。


// 关闭文件函数fclose原型。
int fclose(FILE *fp);
// 调用
fclose(文件指针);

// 单个字符读写
fputc('字符', 文件指针);  
// 向指定文件写入一个字符,写入成功,返回写入的字符,写入失败,返回文本标记EOF。

字符变量 = fgetc(文件指针); 
// 从文件读一个字符放入指定的字符变量中,读取成功返回读入的字符,读取失败,返回文本的结束标记EOF。

常用顺序读写文件函数:

文本文件:

单个字符形式读写文件内容: fputc()写,fgetc()读。

按字符串形式读写文件内容,函数为fputs()写, fgets()读。

固定格式读写文件内容:fprintf()写,fscanf()读。

二进制文件:

按数据块形式读写二进制文件内容,fwrite()写,fread()读。

EOF: 一个宏名,是定义在“stdio.h”的头文件中的一种文件结束标记,值为-1。文本文件中数据以ASCII码形式存储,字符的ASCII码不可能为赋值,因此用-1作为文本文件的结束标记,并定义宏名为EOF。

在读文本文件时,如果读取值为-1则表示文件已经读到文件尾部或读取失败。

但读取二进制文件时不能用-1作为判定结束标志,因其有负值。可以用函数feof()判断文件是否结束。

// 判定文件是否结束函数
feof(文件指针);  // 返回int 值,没有读到文件尾时返回值为0,读到文件尾时返回值为1。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值