达内C语言(day10)

每日英语:

s:string:字符串

回顾:

1. 指针

2. 指针概念

3. 指针定义

4. 指针的初始化:&

5. 指针访问内存:*

6. 空指针和野指针

7. 指针编程规范

8. 指针运算

9. 指针和数组的关系:公式

10. 指针和函数的关系:

11. const关键字

常量指针,指针常量,常量指针常量,常量

12. 无类型指针:void *

13. 指针的综合演练


2. C语言的字符串相关内容

2.1 回顾字符常量:

用单引号括起来,例如:‘A’. ‘1’

注意内存存储的是对应的ASCII码(整数)

字符形式显示的占位符:%c

2.2 字符串定义

由一组连续的字符组成,并且用""括起来,并且最后一个字符必须是’\0’(字符常量)

此字符常量’\0’用于表示字符串的结束,此字符常量对应的ASCII码是0

注意:研究字符串最终还是研究字符串中的每个字符

字符串形式例如:“abcd\0”(完整版)(由字符’a’, ‘b’, ‘c’, ‘d’, '\0’一个挨着一个组成)

一般简写成:“abcd”(简化版,心里清楚,后面还有一个’\0’)

2.3 字符串特点:

2.3.1 字符串的占位符:%s

printf函数输出字符串格式;
printf("%s\n", "abc\0"); //直接跟完整版字符串
或者
pritnf("%d\n", "abc");  // 直接跟简化版字符串
或者
printf("%s\n", 字符串首地址);

2.3.2 字符串占用的内存空间

字符串占用的内存空间是连续的,每个字节存储一个字符串中的ASCII码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DjGhXKE7-1619430369629)(…/…/img/字符串.png)]

2.3.3 多个并列的字符串将来会有gcc帮你合并成一个字符串

例如:

"abc""efg"等价于(合并为)"abcefg"

2.4 字符串和指针那点事

2.4.1 二者关联

定义一个字符指针变量保存字符串的首地址也就是字符指针指向字符串

例如:

char *p = "abcd";   // p指向字符串"abcd",p保存abcd的首地址
通过字符张志珍变量大印字符串:pritnf("%d\n", p);

2.4.2 将来gcc编译器会自动给字符串添加’\0’

完整版:char *p = "abcd\0";
简化版:char *p = "abcd"; //  将来gcc编译器自动会给字符串添加'\0',心里要清楚内存要用5字节

2.4.3 切记切记切记

不能通过字符指针变量来修改字符串中的每个字符,只能查看

因为将来gcc编译器自动将字符串单独放到一个所谓的常量区中(一块特殊的内存,只能看)

例如:

char *p = "abcd";
printf("%s\n", p);  // 查看可以
*(p + 2) = 'C';  // 目标将其中的'c'变'C',不行

2.5 字符串和数组那点事,两种写法:

2.5.1 写法1

char a[] = {'a', 'b', 'c', '\0'};
注意:如果想把a数组当成字符串,必须手动最后添加'\0',如果不添加'\0',a仅仅就是一个包含三个元素的数组而已,而不是	  字符串

2.5.2 写法2

char a[] = "abc";
注意:无需添加'\0',将来编译器自动追加'\0',所以对于此数组将来实际分配4字节内存

2.5.3 注意

不管是那种写法,都可以修改字符数组中的每个字符(因为数组的内存是咱们自己分配的)

例如:

将'c'修改为'C'
a[2] = 'C';
或者
*(a+2) = 'C'
参考代码:string.c

2.6 字符串相关的标准c库函数

注意:需要添加头文件#include

2.6.1 strlen

获取字符串中有效字符的个数(不包括’\0’)

2.6.2 strcat

字符串拼接,将一个字符串连接到另一个字符串的尾部

2.6.3 strcmp

字符串比较函数

2.6.4 strcpy

字符串拷贝函数,用新字符串覆盖老字符串

2.6.5 sprintf

格式化输出函数,按照指定的格式获取字符串

应用:整数转字符串

例如:250->“250”

参考代码:string1.c
/*字符串函数操作演示*/
#include 
#include   // 为了声明大神的字符串操作函数

int main(void)
{
    // strlen:获取字符串中有效字符的个数(不包括'\0')
    char *p1 = "abc";
    printf("%d %d\n", strlen("abc"), strlen(p1));  // 获取有效字符个数

    // strcat:字符串拼接,将一个字符串连接到另一个字符串的尾部
    char a[10] = "abc";   // 一次性分配10字节,只用了3字节,其余都是0
    char *p2 = NULL;
    p2 = strcat(a, "vxyz");   // 将"xyz"放到数组a中并且放到"abc"后面,并且返回数组首地址,其实p2 = a
    printf("%s %s \n", a, p2);   // 打印字符串  
    printf("%d %d\n", sizeof(a), strlen(a)); // 10  7

    // strcmp:字符串比较函数
    int ret = 0;
    ret = strcmp("abc", "abd");  // "abc" 小于 "abd"  ret = -1
    printf("%d\n", ret);

    ret = strcmp("abd", "abc");  // "abc" 大于 "abd"  ret = 1
    printf("%d\n", ret);

    ret = strcmp("abc", "abc");  // "abc" 等于 "abd"  ret = 0
    printf("%d\n", ret);

    char *p3 = "abc";
    char *p4 = "abd";
    ret = strcmp(p3, p4);
    printf("%d\n", ret);   // p3 小于 p4   ret = -1

    // strcpy:字符串拷贝函数,用新字符串覆盖老字符串
    char b[10] = "abc";
    char *p5 = NULL;
    p5 = strcpy(b, "hello");
    printf("%s %s\n", b, p5);

    char *p6 = "world";
    p5 = strcpy(b, p6);
    printf("%s %s\n", b, p5);

    // sprintf:格式化输出函数,按照指定的格式获取字符串
    // char c[50] = {'a','b','c','\0'};
    char c[50] = {0};
    sprintf(c, "%d %g %c %#x", 250, 250.1, 'A', 250);
    printf("%s\n", c);



    return 0;
}

结果:
3 3
abcvxyz abcvxyz 
10 7
-1
1
0
-1
hello hello
world world
250 250.1 A 0xfa

3. 指针数组(实际开发很常用)

3.1 概念

数组中每个元素都是一个地址

3.2 定义的语法格式

数据类型 *数组名[元素个数] = {地址列表};

例如:

int a = 10, b = 20, c = 30;
// 以前做法:定义3个指针变量分别指向a, b, c
int *pa = &a;
int *pb = &b;
int *pc = &c;
如果定义大量变量和对应的指针变量,代码极其啰嗦!
可以采用指针数组优化代码
例如:
int *p[3] = {&a, &b, &c};  // 无需定义大量指针变量
具体后续玩法跟数组一摸一样:
结果:
p[0] = &a = *(p+0)
p[1] = &b = *(p+1)
p[2] = &c = *(p+2)

通过地址获取变量的值
*p[0] = *&a = **(p+0) = 10   (p+0)取出的是地址  *(p+0)取出第一个元素的值=&a  **(p+0)取出的才是数据
*p[1] = *&b = **(p+1) = 20
*p[2] = *&c = **(p+2) = 30

元素个数 = sizeof(p) / sizeof(p[0])
参考代码:array.c
/*指针数组演示*/

#include 

int main(void)
{
    int a = 10, b = 20, c = 30;
    int *p[3] = {&a, &b, &c};
    int len = sizeof(p) / sizeof(p[0]);

    // 打印
    for (int i = 0; i < len; i++)
    {
        // p:数组首地址, p+1第一个元素首地址,*(p+1)取出第一个元素的值=&b,**(p+1) = *&b = 20
        printf("%d %d\n", *p[i], **(p + i));   // 10 20 30
    }

    // 修改值
    for (int i = 0; i < len; i++)
    {
        *p[i] *= 10;
        **(p + i) *= 10;
    }

    // 打印
    for (int i = 0; i < len; i++)
    {
        printf("%d %d\n", *p[i], **(p + i));   // 1000  2000  3000
    }

    return 0;
}

3.3 特殊的指针数组,字符指针数组

  1. 概念:字符指针数组每个元素是一个字符串的首地址

    两种形式:

    char *p[] = {"abc", "efg"};  //第0个元素是字符串"abc"的首地址,第一个元素是字符串"efg"首地址
    注意:指针数组中存的不是字符,一定是字符的首地址
    等价于
    char *p1 = "abc";
    char *p2 = "efg";
    char *p3[] = {p1, p2};
    注意:其中字符也是不能修改
    
参考代码:array1.c
/*字符指针数组演示*/

#include 
int main(void) {
    // 形式1
    char *p[] = {"abc", "efg"};
    int len = sizeof(p) / sizeof(p[0]);

    // 打印
    for(int i = 0; i < len; i++)
    {
        printf("%s, %s\n", p[i], *(p+i));
    }

    // 不可修改
    //*(p[0] + 1) = 'B';
    //*(p[1] + 2) = 'G';

    //形式2
    char *p1 = "abcd";
    char *p2 = "efgh";
    char *p3[] = {p1, p2};
    
    // 打印
    for(int i = 0; i < len; i++)
    {
        printf("%s, %s\n", p3[i], *(p3+i));
    }


    return 0;
}

结果:
abc, abc
efg, efg
abcd, abcd
efgh, efgh

第八课:预处理(核心)

1. 回顾c源文件编译三步骤

预处理:替换,拷贝的过程

​ 命令:gcc- E -o xxx.i xxx.c

只编译不链接:将预处理之后的源文件单独翻译成CPU能够识别的可执行文件

​ 命令:gcc -c -o xxx.o xxx.i

链接:将单独编译完成的可执行文件添加库函数相关的代码(printf,strcmp,strcpy),生成最终的可执行文件

​ 命令:gcc -o xxx xxx.o

2. 预处理(涉及的指令代码#开头,后面不跟分号)分类:

2.1 头文件包含预处理指令:

#include,又分两类:

2.1.1 #include <头文件>

语义:在预处理时,gcc会自动到操作系统的/usr/include目录下找到要包含的头文件

​ 如果找到了,将头文件里面的所有内容拷贝到源文件中

例如: #include

2.1.2 #include “头文件”

语义:在预处理时,gcc首先在当前目录下找要包含的头文件,如果找到了那就拷贝

​ 如果没有找到,gcc再去/usr/include目录下找到要包含的文件

2.1.3 特殊情况(在别的目录下)

有可能头文件既不在/usr/include目录下,又不再当前目录下可能在别的目录下

只需通过一下命令来找到要包含的头文件(开发时很常用)

gcc .... xxx.i xxx.c -I 头文件所在路径
例如:
gcc -o A A.C -I /home/tarena/stdc
将来gcc自动到-I制定的路径下找头文件

2.2 宏定义预处理指令:#define

  1. 宏定义预处理指令又分两种:常量宏和参数宏(又称宏函数)

  2. 常量宏

    1. 定义常量宏的语法格式:#define 宏名 (值)

      例如:#define PI (3.14)

      语义:定义宏PI,其值为3.14

      结果:将来程序处理中如果使用了宏PI,那么在预处理时,gcc会将宏PI全部替换成3.14然后进 行运算

      建议:宏名用大写

      优点:提高代码的可移植性,将来代码如果要改的,改的的工作量很少

      案例:利用宏实现计算圆的周长和面积
      
      参考代码:circle.c
      
      编译命令:gcc -E -o circle.i circle.c  预处理,此时gcc会将宏PI替换成3.14
      		vim circle.i   // 跳转到文件的最后,观察PI是否变成3.14
      		gcc -o circle.i circle.o  编译
      		gcc -o circle circle.i  链接
      		./circle  运行
      		
      /*常量宏演示*/
      /*操作命令
      gcc -E -o circle.i circle.c
      vim circle.i   打开预处理文件,跳转到最后,观察是否替换
      gcc -o circle circle.i
      ./circle
      */
      
      #include 
      
      /*定义宏*/
      #define     PI      (3.14)
      int main(void)
      {
          // double r = 10;
          double r = 10;
          printf("周长=%lf\n", 2*PI*r);  //%lf是按double类型进行输出
          printf("面积=%lf\n", PI*r*r);
      
          return 0;
      }
      
  3. 参数宏(宏函数)

    语法格式:#define 宏名(宏参数) (宏值)

    例如:

    #define SQUARE(x)	((x) * (x))
    #define SUB(x, y)	((x) - (y))
    

    语义:在预处理时,gcc先将宏参数替换成实际值,然后将代码中的宏名最终全部替换成宏值

    注意:宏值里面的宏参数不要忘记圆括号()

    优点:宏函数比普通函数的代码执行效率要高

    ​ 宏函数将来在预处理时做了替换,程序运行时直接运行

    ​ 函数涉及调用,传参的过程,这个过程是需要消耗CPU资源

    编译命令:	gcc -E -o define.i define.c
    			gcc -c -o define.o define.i
    			gcc -o define define.o
    			./define
    

    案例:宏函数演练,参考代码:define.c

2.3 特殊的预处理指令#和##

2.3.1 # 作用

将后面跟的宏参数转换成字符串

例如:#N替换的结果是"N"

参考代码:#define.c

2.3.2 ## 作用

现将其后面的宏参数替换,然后与前面的部分粘连在一起,最终做为宏值进行替换

例如:

id##N替换的结果是id1

参考代码:#define.c

/*宏函数演示*/
#include 
/*定义宏函数SQUARE求平方*/
#define SQUARE(x) ((x) * (x))
/*定义宏函数SUB求两个数相减*/
#define SUB(x, y) ((x) - (y))
/*清0置1宏函数*/
#define CLEAR_BIT(data, bit) (data &= ~(1 << n))
#define SET_BIT(data, bit) (data |= (1 << n))
/*定义宏函数PRINT*/
#define PRINT(N) (printf(#N "=%d\n", N))
/*定义宏函数ID*/
#define ID(N) id##N     // id##N 不要加括号 但是加了也没报错

int main(void)
{
    printf("%d\n", SQUARE(10));
    printf("%d\n", SQUARE(3 + 7));

    int c = 400, d = 200;
    printf("%d\n", SUB(c, d));

    int a = 0x55, n = 0;
    CLEAR_BIT(a, n);
    printf("a = %#x\n", a);
    SET_BIT(a, n);
    printf("a = %#x\n", a);

    int b = 10, e = 20;
    PRINT(b); // printf("b""=%d\n", b)等价于printf("b = %d\n", b)
    PRINT(e);

    int ID(1) = 100, ID(2) = 200;  // 替换为:int id1 = 100, id2 = 200;
    printf("%d %d\n", ID(1), ID(2));
    return 0;
}

2.4 编译器定义好的宏

(直接拿来用),实际开发必用,主要用于调试,log日志记录

注意:都是两个下划线

占位符 含义
__ FILE __ %s 表示文件名
__ LINE__ %d 表示行号
__ FUNCTION __ %s 表示函数名
__ DATE __ %s 表示文件创建日期
__ TIME __ %s 表示文件创建时间
实际开发log日志使用公式;
printf("在代码的这里错误:%s, %s, %s, %s, %d",出现了野指针的非法访问.\n",
	__DATE__,__TIME__,__FILE__,__FUNCTION__,__LINE__);

参考代码:define.c

/*预定义宏演示*/
/*编译命令:gcc -DSIZE=5 -DEND=\"很开心\" -o define define.c windows下无法通过编译
    但是ubuntu下可以
*/
#include 
int main(void)
{
    int a[SIZE] = {0};
    // 赋值
    for(int i = 0; i < SIZE; i++)
    {
        a[i] = i + 1;
    }
    // 打印
    for(int i = 0; i < SIZE; i++)
    {
        printf("a[%d] = %d\n", i, a[i]);
    }
    printf("%s\n",END);
    return 0;
}

2.5 用户可以动态预定义宏

通过gcc的-D选项来指定宏

作用:程序在编译的时候将-D选项指定的宏传递给程序使用

注意:如果宏是一个字符串,那么需要用\"转义

例如:

gcc -DSIZE=250 -DWELCOME=\"达内"
代码使用:printf(%d %s\n, SIZE, WELCOME);

参考代码:define1.c

2.6 条件编译预处理指令(大型软件代码用的非常多)

条件编译:符合条件的,代码就编译,不符合条件的,代码不编译,让代码删除消失

#if // 如果

#ifdef // 如果定义了

#ifndef // 如果没有定义

#elif //否则如果

#else //否则

#endif // 必须和#if或者#ifdef或者ifndef配对使用

参考代码:if.c

/*条件编译预处理指令演示*/
/*编译命令:gcc -E -o if.i if.c 
           gcc -o if if.i
           ./if            没有条件的  if.i中main函数什么也没有
           gcc -DA=1 -E -o if.i if.c 
           gcc -o if if.i
           ./if             加了条件  if.i中main函数里有printf("1\n");
*/

#include 

int main(void)
{
    //#if演示
#if A == 1 // 如果A等于1,条件成立,将会编译printf1,否则不编译
    printf("1\n");
#endif // 跟#if配对

    //#if...#else演示
#if B == 2
    printf("2\n");
#else
    printf("3\n");
#endif

    // #ifdef/#ifndef.....#else演示
    // gcc -DC -E -o if.i if.c    C可以不传数值
    // gcc -o if if.i
    // ./if
#ifndef C // 如果没有定义了宏C,编译printf4,定义了宏C 编译printf5

    //#ifdef C  // 如果定义C宏,编译printf4,否则编译printf5
    printf("4\n");
#else // 可以不加
    printf("5\n");
#endif

    // #if defined() ...#else演示
    // gcc -E -o if.i if.c          7
    // gcc -DD -E -o if.i if.c      6
    // gcc -DE -E -o if.i if.c      8
    // gcc -DD -DE -E -o if.i if.c  6   原因是:编译器是从上往下执行的,最上面的数字是6,所以就是6
    // gcc -DE -DD -E -o if.i if.c  6   原因是:编译器是从上往下执行的,最上面的数字是6,所以就是6
#if defined(D)
    printf("6\n");
#elif !defined(D) && !defined(E)
    printf("7\n");
#else
    printf("8\n");
#endif

    return 0;
}

实际开发的演示代码:

利用条件编译可以让一套代码支持不同种类的CPU(X86,ARM,DSP…等),也不会增大代码的体积

否则要分别给没类CPU都写一套代码,非常繁琐

void A(void)
{
	#if ARCH==X86
		只编译X86的CPU代码
	#elif ARCH==ARM
		只编译ARM的CPU代码
	...
	#else
		只编译DSP的CPU代码
	#endif
}
编译生成ARM的CPU代码:gcc -DARCH=ARM xxx.i xxx.c

3. 大型程序软件基本框架

3.1 掌握头文件卫士

切记:以后编写任何头文件(以.h结尾)代码框架必须如下

vim A.h 添加
#ifndef __A_H	//这是头文件卫士,宏名一般跟头文件名同名
#define __A_H
	这里添加头文件要包含的代码内容
#endif		//跟#ifndef配对

头文件作用:防止头文件的内容(变量,函数等)重复定义

3.2 问:头文件卫士工作原理?

答:分析原理(了解即可)

例如:

没有头文件卫士保护的情形:

vim a.h 添加
int a = 250;	//定义一个变量
保存退出

vim b.h 添加
#include "a.h"
保存退出

vim main.c 添加
#include 
#include "a.h"
#include "b.h"

int main(void)
{
	printf("a = %d\n", a);
	return 0;
}

gcc -E -o main.i main,c
打开main.i 得到:
int a = 250;
int a = 250;  // 显然重复定义了,后续编译肯定不通过

利用头文件卫士解决

利用头文件卫士解决
vim a.h 添加
#ifndef __A_H
#define __A_h
int a = 250;  // 定义变量
#endif

vim b.h 添加
#ifndef __B_H
#define __B_h
#include "a.h"
#endif

vim main.c 添加
#include 
#include "a.h"
#include "b.h"
int main(void)
{
	printf("a = %d\n", a);
	return 0;
}

gcc -E -o main.i main,c
打开main.i 得到:
int a = 250;  // 只有一个了

达内C语言(day10)_第1张图片
关注公众号,获取更多精彩内容

你可能感兴趣的:(c语言)