嵌入式应用工程师——C语言基础

Posted ⁢稚子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了嵌入式应用工程师——C语言基础相关的知识,希望对你有一定的参考价值。

数组

  • int a[10]=0; 定义整型数组a,里面有10个元素
  • char ch[20]=0; 定义字符数组ch,里面有20个字符

函数

函数的形参和实参的特点:形参变量只有在被调用时才分配内存单元,在调用结束时,即刻释放所分配的空间。

  1. 函数命名方法,驼峰命名法:单词首字母大写,例如:void FindMax()
  2. 外部变量、局部变量、全局变量:局部变量为在函数中定义的变量,全局变量为在函数外定义的变量,外部变量(在自定义函数中,可以采用extern 调用全局变量)

main函数

实际上,main函数可以带参数,这个参数可以认为是main函数的形参。C语言规定main函数的参数只能有2个,习惯上这两个参数写为argc和argv。arg(译:参数)
C语言还规定argc(第一个形参)必须是整型,argv(第二个形参)必须是指向字符串的指针数组。

void main(int argc, char* argv[])

static

  1. 作为变量的存储类型说明符 static int a = 0:定义静态变量,只会在程序第一次执行到该语句时被初始化,只会被执行一次,其作用域仅限于定义该变量的函数内部或者文件内部,不会被其他函数或文件所访问。
  2. 作为函数的存储类型说明符 static Function():定义静态函数,这种函数只能在定义它的源文件中被调用,其他源文件中不能调用该函数。静态函数可以避免函数名的冲突,同时也可以使得程序的安全性得到提升。(比如,当在两个文件中,定义了同一个函数名,可以使用static进行修饰函数)
  3. 作为外部变量和函数的作用域限定符:static可以限制外部变量和函数的作用域,使得他们只能在当前文件内部被访问。这样可以避免不同文件之间的变量或函数名的冲突,增强程序的可维护性。
    总结:static,可以用于控制变量和函数的生命周期、作用域和访问权限,从而使得程序更加安全、易于维护。static修饰的变量只会被初始化一次,之后再运行到staic int a= 0时,会自动跳过。static修饰的局部变量相当于全局变量。

预处理命令

条件编译

#ifdef 标识符
    程序段1
#else
    程序段2
#endif

防止头文件被多次引用

#ifndef LED_H
#define LED_H
*
*
#endif

// 或者这样
#pragma once //需要编译器的支持。

指针

**指针(Pointer)其实就是地址,地址就是内存编号。**其一般形式为:*变量名

int* p1;    // 表示p1是一个指针变量,它的值是某个整型变量的地址。

char *pc = "C Language"; // 并不是把整个字符串装入指针变量,而是把存放该字符串的字符数组的首地址装入指针变量pc

/*定义一个函数*/
void Function(int *pt)
    int a = *pt + 1;

// 在调用该函数时,需要传递进去变量的地址,&a
  • 数组是由连续的一块内存单元组成,数组名就是这块连续内存单元的首地址。
  • 通过指针引用数组元素:
    • 下表法:用a[i]形式访问数组元素
    • 指针法:采用*(a+i)或*(p+i)形式,用间接访问的方式来访问数组元素。*(p+5)或 *(a+5)就是a[5],因为数组名就是数组元素的首地址,相当于a[0]

char、char*、char a[]、char* a[]

C语言中规定数组代表所在内存位置的首地址.
C语言中没有String这个数据类型,一般用char *表示字符串,或者用char A[]字符数组表示字符串
C语言中操作字符串是通过它在内存中的存储单元的首地址进行的,这是字符串的终极本质。

  • char *s = "China";s为一个地址,字符串常量的本质表现是它的第一个字符的地址。
  • char *和char a[]的本质区别:当定义char a[10]时,编译器会给数组分配10个单元,每个单元的数据类型为字符。而定义char *s时,这是个指针变量,只占4个字节,用来保存一个地址。
    总结:char *s只是一个保存字符串首地址的指针变量,char a[]是许多连续的内存单元,每个元素都是char类型
  • char *s = "hello";等价于char str[] = "hello"
字符串的表示方法

字符串没有单独的类型,所以,它可以用数组或是指针来表示。
用数组表示:

char str[] = 'b','i','t','\\0';//第一种表示方法
// 第一种写法不常用,注意加结束符 \\0
char str[] = "bit";//第二种表示方法,实际其中存储为 b i t \\0
char *parray[3];  // 存储3个字符串                

用指针表示:

char *str2 = "bit";

数组指针和指针数组

前面两个字为修饰词,类型为后面的两个字。优先级:() > [] > *

  • (*p)[n]:根据优先级,先看括号内,则p是一个指针,指向一个一维数组,数组长度为n,即数组的指针,数组指针
  • *p[n]:根据优先级,先看[],则p是一个数组,再结合 *,这个数组的元素是指针类型,共n个元素,即指针的数组,指针数组
  • 1个地址(指针)占4个字节
  1. 数组指针:数组的指针。例如:int (*p)[3]; // p是一个指针,指向一个数组(数组元素的首地址),数组内是3个整型数据
  2. 指针数组:指针的数组,是一个数组,内容为指针。例如:int *p[3]; // p是一个数组,里面有3个元素,每个元素都是指针,指向整型数据
#include <stdio.h>

int main(int argc, char *argv[])

    const int max = 3;
    static array[] = 1, 2, 3;

    for (int i = 0; i < max; i++)
    
        printf("array[%d] = %d\\n",i,array[i]);
    

    int *p[max];
     for (int i = 0; i < max; i++)
    
        p[i] = &array[i];
        printf("array[%d] = %p\\n",i,&array[i]);
    
    return 0;

  1. 指针的指针:char **p; 相当于 char *(*p)
  • 变量赋值:char *s = “C Language”; // s是一个指向字符串的指针变量,把字符串的首地址赋予给s
    指针变量赋值:
    | p = &a | 把变量a的地址赋值给p |
    | ------------- | ------------------------------- |
    | p = array | 将数组array的首地址传给p |
    | p = &array[i] | 将数组array第i个元素的地址赋给p |

指针应用实例

#include <stdio.h>

int main(void)

    int a = 1;
    int *p = &a; // 绛変环浜庢崲涓€琛屽啓 p = &a
    printf("变量a的地址为: %p\\n", p);
    printf("变量a的值为: %d\\n", a);
    printf("变量a的值为: %d\\n", *p);
    return 0;

// ==================指针的运算
    static int array[] = 1, 2, 3;  // 这样每次执行时,cpu就不会再重新分配地址了
    int i;
    int *p = array; // 数组名本身就是一个指针,代表数组元素的首地址

    for (i = 0; i < 3; i++)
    
        printf("array[%d]= %p\\n", i, p + i);
        printf("array[%d]= %d\\n", i, *(p + i)); // 输出数组的值
    
// 指针的指针
#include <stdio.h>

int main(int argc, char *argv[])

    int a = 1;
    int *p1;
    int **p2; // 相当于*(*p2)

    p1 = &a;
    p2 = &p1;

    printf("a= %d\\n", a);
    printf("p1 = %p\\n", p1);
    printf("p1 = %d\\n", *p1); // *表示解引用

    printf("p2 = %p\\n", p2);  //     p2 = &p1;
    printf("**p2 = %d\\n", **p2); // *表示解引用
    return 0;

指针传参

// 在调用时,采用&取地址进行变量传递
#include <stdio.h>

float average(int *array, int size)

    float sum = 0;
    for (int i = 0; i < size; i++)
    
        sum += array[i];
    
    float avg = sum / size;
    return avg;


int main(int argc, char *argv[])

    int student[] = 10,20,30;
    float avg = average(student,3); // 数组的名字就是一个指针
    printf("平均值:%f\\n",avg);
    return 0;

函数返回指针变量

#include <stdio.h>
#include <time.h>

int *GetNumber()

    static int array[10] = 0;             // C语言不支持调用函数时返回局部变量的地址,所以加一个static
    int size = sizeof(array) / sizeof(int); // 数组大小的计算
    srand(time(NULL));                      // 随机种子

    for (int i = 0; i < size; i++)
    
        array[i] = rand();
        printf("%d\\n", array[i]);
    
    return array; // 数组名就是一个指针地址


int main()

    int *p = GetNumber();

    for (int i = 0; i < 10; i++)
    
        printf("*(p+[%d]) =%d\\n ", i, *(p + i));
    
    return 0;

结构体、联合体、枚举

结构体(struct)、联合体(union)、枚举(enum)

结构体

  • 结构体指针变量的说明和使用:结构指针变量中的值是所指向的结构变量的首地址。 struct 结构名* 结构指针变量名
struct stu *pstu;
  • 结构名只能表示一个结构形式,编译系统并不对它分配内存空间,结构指针变量访问成员:
(*pstu).num; // 或pstu->num

为了提高程序的运行效率,最好的办法是使用指针,即用指针变量作为函数参数进行传送,这时由实参传向形参的只是地址,从而减少了时间和空间的开销。

结构体应用实例

示例:结构体赋值

#include <stdio.h>

typedef struct

    int num;
    char name[20];
    char sex;
    int age;
    float score;
    char address[30];
 Student;

int main()

    Student stu = 0; // 结构体实例化
    // stu.name = "张三";  报错,字符串采用下面这种方法进行赋值
    strcpy_s(stu.name, "张三"); // _s表示安全类型
    stu.age = 1;
    stu.sex = 'M'; // 字符类型赋值

    return 0;

示例:结构体指针的应用(重要)

#include <stdio.h>

typedef struct

    int num;
    char name[20];
    char sex;
    int age;
    float score;
    char address[30];
 Student;

/*采用指针的方式进行结构体实例化,调用时可以提高程序的运行效率*/
Student *stu;

int main()

    stu->age = 1;
    stu->sex = 'M';
    strcpy(stu->name, "张三");
    return 0;

示例:结构体指针形参

#include <stdio.h>

typedef struct

    int num;
    char name[20];
    char sex;
    int age;
    float score;
    char address[30];
 Student;

Student stu;

// 如果不用指针,相当于把整个结构体传入过去,使用指针只传递地址就行,大大提升了程序的运行速度
void Print_Student(Student *pt)

    printf("age = %d\\n", pt->age);
    printf("name = %s\\n", pt->name);


int main()

    stu.age = 1;
    strcpy(stu.name, "张三");
    Print_Student(&stu);
    return 0;

共用体(联合体)

结构体与共用体的区别:

  1. 结构体:每个成员变量都有自己的内存地址,可以独立地被访问和修改
  2. 共用体:所有成员变量共享同一块内存空间(共用体的大小等于其最大成员的大小),因此只能同时访问一个成员变量,修改一个成员变量的值将影响其他成员变量的值。
  3. 结构体的成员变量可以同时存在多个,它们之间没有任何关系;而共用体的成员变量虽然在同一块内存中,但是它们之间的关系是互斥的,每次只有一个成员变量可以被访问

枚举

枚举只是一种基本的数据类型,不是一种构造函数,枚举值是常量,不是变量,可以让数据简洁和已读。注:第一个枚举成员的值为整型0(也可以把第一个元素定义为1),后面的成员值+1。
定义形式

enum 枚举名枚举值表;
例如:
enum weekdaysun,mou,tue,wed,thu,fri,sat;
enum weekday a,b,c;
或者为:
enum weekdaysun,mou,tue,wed,thu,fri,sat a,b,c;
或者为:
enum sun,mou,tue,wed,thu,fri,sat a,b,c;
#include <stdio.h>
enum DAY
      MON=1, TUE, WED, THU, FRI, SAT, SUN
 day;
int main()
    // 遍历枚举元素
    for (day = MON; day <= SUN; day++) 
        printf("枚举元素:%d \\n", day);
    

动态内存分配函数

最常用的内存管理函数是malloc,调用格式:(类型说明符*) malloc(size)
功能:在内存的动态存储区中分配一块长度为 size 字节的联系区域,函数的返回值为该区域的首地址。例如:

pc = (char*)malloc(100);
表示分配100个字节的内存空间,并强制转换为字符数组类型,函数的返回值为指向该字符数组的指针,把该指针赋予指针变量pc

释放内存空间函数free

调用形式:free(void *ptr);
功能:释放ptr所指向的一块内存空间,被释放区应是由malloc或calloc函数所分配的区域。

动态内存管理

malloc():memory, allocation,内存分配

位域

位域的分配方式分配空间只能用在结构体和类中。

// 进行位域划分,一个int为4个字节,32bit
// 使用方法,在变量名后加一个:即可
struct Date
  int year:12;  //分配12个bit
  int month:4;  // 分配4个字节
  int day:5;  // ....
  int hour:5;
  int minute:6;
  int second:6;
;

内存分区与函数类型

由C/C++编译的程序占用的内存分为以下几个部分:

  1. 栈区(stack):由编译器自动分配释放,存放函数的参数值、局部变量的值等
  2. 堆区(heap):程序员分配释放内存,若程序员不释放,程序结束时可能由OS回收。注:与数据结构中的堆不同,分配方式类似于链表
  3. 数据区:主要包括静态全局区(static)和常量区
  4. 代码区:存放函数体的二进制代码

内存操作函数

  1. malloc()函数:memory allocation,动态内存分配,用于申请一块连续的指定大小的内存,new()也是申请动态内存
  2. calloc()函数:malloc 和 calloc 之间的不同点是,malloc 不会设置内存为零,而 calloc 会设置分配的内存为零。
  3. free()函数:释放内存
  4. memcpy()函数:拷贝内存
  5. memmove()函数:memove(dest,src,3) // 把src中前3个字符复制给dest
  6. memset()函数:memset(str,‘A’,4) // 把字符串的前4为设为A,注意要填写ASCII

深拷贝与浅拷贝

  • 浅拷贝:多个指针指向同一段内存;直接赋值,例如定义两个字符串,parray[i] = myarray; int a = b;这些都属于浅拷贝,浅拷贝使用比较频繁
  • 深拷贝:每个指针指向单独的内存;例如strcpy(dst,str);
    假设B复制了A,当修改A时,看B是否会发生变化。如果B也跟着变了,则是浅拷贝,如果B没变,则是深拷贝。

嵌入式C语言面向对象编程 --- 封装

大部分使用 C 语言进行开发的工程师,在接触更高级的编程语言之前,都认为 C 语言是面向过程的。事实也是如此,对于一些小规模的单片机应用程序,一般都是使用“面向过程”的思维进行单片机C语言编程开发。

但是,如果是需要用C语言开发一些规模比较大的软件的时候,比如操作系统内核,文件系统底层,数据库底层,等等,这个时候,就需要用面向对象的思想去考虑和设计整个软件框架了。

嵌入式Linux的内核,虽然是使用 C 语言编写的,但里面的设计大部分都使用了面向对象的编程思想。

 图片来源公众号:码农翻身

很多单片机工程师或者嵌入式Linux驱动初学者,有时候会觉得驱动入门特别困难,很大一部分原因是,他们会用“过程式思维”去尝试学习驱动框架和内核框架,而非从“整体对象”的思维方向出发,这样容易导致水土不服。

任何编程语言只是一种工具,而编程思想是指导我们用好这个工具的关键。C 语言只是工具,而面向对象是一种编程思想,用来指导我们如何用从另一种思维模式去使用 C 语言。

以上是关于嵌入式应用工程师——C语言基础的主要内容,如果未能解决你的问题,请参考以下文章

C/C++在嵌入式中地位不保,Rust将成为更好的“备胎”?

嵌入式C语言面向对象编程 --- 总结

C语言概貌

传统的嵌入式C语言程序员快要灭绝了?

嵌入式工程师讲述:C语言从小菜鸡到老司机

嵌入式开发工程师学习线路