(嵌入式八股)第1章 C语言(一)
1.1 运算符
运算符高低转换(类型转换)
C语言中的运算符会遵循一定的规则来进行类型转换,通常遵循“低等类型向高等类型转换”的原则。这是因为高等类型能够表示更多的值范围和精度。例如:
- 如果你有
3.0 * 6
,这里3.0
是浮点数,6
是整数。编译器会自动将6
转换为浮点数,以确保乘法操作的结果是浮点型(float
或double
)。 - 类型的等级关系如下:
这意味着对于数字类型,double
是最高等级,而 char
和 short
是较低等级。
a
是一个浮点数(float
类型),而 b
是一个整数(int
类型)。a * b
中,运算符 *
的作用是执行乘法,但由于一个操作数是浮点数,编译器会自动将 b
从 int
类型转换为 float
,以确保运算结果符合浮点数的精度要求。b
会被提升为 float
类型,然后执行浮点乘法,结果是一个 float
类型。运算符优先级
运算符的优先级决定了在没有括号改变运算顺序时,哪些运算先进行。C语言中的运算符优先级规则如下:
- 从高到低优先级:() (括号)[] (数组下标)!(逻辑非)自增自减运算符 (++, --)sizeof(取sizeof)* (乘法),& (取地址),& (按位与)
- 算术操作符(如 +, -, *, /, %)优先级较高,但低于数组下标、取地址等。
- 关系操作符(如 ==, !=, <, <=, >)优先级较算术操作符低。
- 逻辑操作符(如 &&, ||)优先级更低。
- 赋值操作符(如 =, +=, -= 等)优先级最低,且赋值操作符的结合方向是从右到左。所有赋值运算符的运算顺序都是从右向左进行的。
*
) 的优先级高于加法 (+
)。3 * 2 = 6
,然后执行加法 5 + 6 = 11
。11
。运算符的结合方向
运算符的结合方向决定了当表达式中存在多个相同优先级的运算符时,如何进行计算。C语言中的运算符大多是从左到右结合的,但有些例外:
- 从右到左结合的运算符:
- 赋值运算符(=, +=, -= 等)
- 条件运算符(?:)
- 自增自减运算符(++ 和 -- 在某些情况,如后缀自增自减时,也有从右到左的特性)
- 从左到右结合的运算符:所有其他二元运算符(如 +, -, *, / 等)
-
和 +
运算符的优先级是相同的,且它们是从左到右结合的。10 - 2 = 8
,然后执行 8 + 3 = 11
。11
。运算符优先级表
1 | [] | 数组下标 | 数组名[常量表达式] | 左到右 | |
1 | () | 圆括号 | (表达式)或函数名(形参表) | 左到右 | |
1 | . | 成员选择(对象) | 对象.成员名 | 左到右 | |
1 | -> | 成员选择(指针) | 对象指针->成员名 | 左到右 | |
2 | - | 负号运算符 | -表达式 | 右到左 | 单目运算符 |
2 | (类型) | 强制类型转换 | (数据类型)表达式 | 右到左 | |
2 | ++ | 自增运算符 | ++变量名 或 变量名++ | 右到左 | 单目运算符 |
2 | -- | 自减运算符 | --变量名 或 变量名-- | 右到左 | 单目运算符 |
2 | * | 取值运算符 | *指针变量 | 右到左 | 单目运算符 |
2 | & | 取地址运算符 | &变量名 | 右到左 | 单目运算符 |
2 | ! | 逻辑非运算符 | !表达式 | 右到左 | 单目运算符 |
2 | ~ | 按位取反运算符 | ~表达式 | 右到左 | 单目运算符 |
2 | sizeof | 长度运算符 | sizeof(表达式) | 右到左 | |
3 | / | 除 | 表达式/表达式 | 左到右 | 双目运算符 |
3 | * | 乘 | 表达式*表达式 | 左到右 | 双目运算符 |
3 | % | 余数(取模) | 整型表达式%整型表达式 | 左到右 | 双目运算符 |
4 | + | 加 | 表达式+表达式 | 左到右 | 双目运算符 |
4 | - | 减 | 表达式-表达式 | 左到右 | 双目运算符 |
5 | << | 左移 | 变量<<表达式 | 左到右 | 双目运算符 |
5 | >> | 右移 | 变量>>表达式 | 左到右 | 双目运算符 |
6 | > | 大于 | 表达式>表达式 | 左到右 | 双目运算符 |
6 | >= | 大于等于 | 表达式>=表达式 | 左到右 | 双目运算符 |
6 | < | 小于 | 表达式<表达式 | 左到右 | 双目运算符 |
6 | <= | 小于等于 | 表达式<=表达式 | 左到右 | 双目运算符 |
7 | == | 等于 | 表达式==表达式 | 左到右 | 双目运算符 |
7 | != | 不等于 | 表达式!=表达式 | 左到右 | 双目运算符 |
8 | & | 按位与 | 表达式&表达式 | 左到右 | 双目运算符 |
9 | ^ | 按位异或 | 表达式^表达式 | 左到右 | 双目运算符 |
10 | | | 按位或 | 表达式|表达式 | 左到右 | 双目运算符 |
11 | && | 逻辑与 | 表达式&&表达式 | 左到右 | 双目运算符 |
12 | || | 逻辑或 | 表达式||表达式 | 左到右 | 双目运算符 |
13 | ?: | 条件运算符 | 表达式1? 表达式2: 表达式3 | 右到左 | 三目运算符 |
14 | = | 赋值运算符 | 变量=表达式 | 右到左 | |
14 | /= | 除后赋值 | 变量/=表达式 | 右到左 | |
14 | *= | 乘后赋值 | 变量*=表达式 | 右到左 | |
14 | %= | 取模后赋值 | 变量%=表达式 | 右到左 | |
14 | += | 加后赋值 | 变量+=表达式 | 右到左 | |
14 | -= | 减后赋值 | 变量-=表达式 | 右到左 | |
14 | <<= | 左移后赋值 | 变量<<=表达式 | 右到左 | |
14 | >>= | 右移后赋值 | 变量>>=表达式 | 右到左 | |
14 | &= | 按位与后赋值 | 变量&=表达式 | 右到左 | |
14 | ^= | 按位异或后赋值 | 变量^=表达式 | 右到左 | |
14 | |= | 按位或后赋值 | 变量|=表达式 | 右到左 | |
15 | , | 逗号运算符 | 表达式,表达式,... | 左到右 |
1.2 C语言标识符的命名规则
标识符的基本构成
标识符必须符合以下构成规则:
- 必须以字母(aZ)或下划线(_)开头,后面可以是字母、数字(0~9)或下划线。
- 有效标识符例子:
- count
- total_sum
- _myVariable
- Var1
- 无效标识符例子:
- 1variable(不能以数字开头)
- @sum(不能包含特殊字符)
标识符区分大小写
C语言的标识符是区分大小写的。这意味着 Variable
和 variable
被视为不同的标识符。
这里,Variable
和 variable
是两个不同的变量。
标识符长度
- C89标准规定标识符长度不得超过31个字符。
- C99标准规定标识符长度不得超过63个字符。
通常,编译器会根据所支持的标准来决定允许的最大标识符长度。
关键字不能作为标识符
C语言有一组关键字(如 int
, if
, return
等),它们有特定的意义,因此不能被用作标识符。
无效标识符:
有效标识符:
自定义标识符的命名习惯
虽然C语言允许使用任意字符来命名标识符,但最好使用具有明确意义的名字,这有助于代码的可读性和维护。例如,使用 counter
代替 x
,使用 totalSum
代替 sum
。
避免使用单个字母如 a
、b
或过于简短的名字,这样的名字虽然合法,但很难理解其含义。
标识符命名规范是编写清晰、易于理解代码的基础。
遵循这些规则,可以确保代码的可维护性和可读性。在命名时,使用具有描述性的名称将大大提高代码的质量。
1.3 字符数组初始化
在C语言中,数组的初始化有不同的方式,尤其是对于字符数组(字符串)的初始化。
字符数组初始化
在C语言中,字符串实际上是字符数组,其中的每个字符都有一个对应的元素,并且每个字符串都会以一个特殊的字符 \0
(空字符)结束,表示字符串的结束。
这里,arr1
是一个字符数组,包含三个元素:a
,b
和 c
。但是,没有显式添加空字符 \0
,所以这个数组表示的是不完整的字符串。
这里,arr2
是一个字符数组,包含四个元素:'a'
,'b'
,'c'
和 '\0'
。在C语言中,字符串常量 "abc"
会自动在最后加上 \0
字符,标识字符串的结束。
"abc"
中,编译器会自动将其转换为 {'a', 'b', 'c', '\0'}
,其中 \0
是字符串的结束标志。arr2
数组的实际内容是四个字符,包括 \0
。数组的隐式和显式初始化
- 隐式初始化:在声明数组时,如果使用字符串常量,编译器会自动在末尾添加 \0 字符。
- 显式初始化:如果要显式添加
\0
字符,可以通过手动指定数组元素来初始化数组。
数组大小
- 对于数组初始化,数组的大小可以根据初始化的内容自动推导。例如:
数组大小为 6,因为字符串 "Hello" 需要 5 个字符 + 1 个 \0。
- 如果显式指定了数组大小,但实际存储超过了界限,会引发未定义的行为,这样子很危险。例如:
实际元素数为 6(包括 \0
终止符)。
总结
- 字符串常量会自动加上
\0
,所以字符数组会包含此字符作为结尾。 - 数组的大小可以通过显式或隐式方式来确定,但无论是哪种方式,字符数组都会包含
\0
字符以标识字符串的结束。 - 如果你不显式地在字符数组中添加
\0
,则它可能无法正确地表示字符串。
1.4 数组名和&数组名所表示的地址区别
数组名实际上就是数组的首元素地址。但是有两种情况例外:一种是用sizeof()去求数组的大小的时候;
还有一种情况是在&整个数组的时候(就是取整个数组的地址),
这两种情况下,数组名代表的是整个数组的地址。
我们知道,指针加减一个常数表示的是下一个元素。
那么,直接一个a表示的是数组首元素的首地址;其下一个元素就是数组a的第二个元素。
而&a表示的是整个数组的首地址;其下一个元素是数组后面的地址。
至此,我们就解释清楚了这两者的关系。
数组元素访问的两种形式:一种是用数组的下标访问,还有一种是用指针访问。
一维数组:a[ i ]和 *(a+ i)
二维数组:a[ i ][ j ]和 *(*(a+ i) + j)
数组指针 :
1.&a[i][j] 数组元素a[i][j]的地址
2.&a[i] 数组元素a[i][0]的地址
3.a[0]+j 数组元素a[0][j]的地址
4.*(*(a+i)+j) 数组元素a[i][j]的值
5.*(a[i]+j) 数组元素a[i][j]的值
(*p)[5]:p[i]等于*(p+i),c[4][5]:p[i]等于c[i], c[i][j]等于*(*(p+i)+j)等于*(p[i]+j)
1.5 C语言形参跟实参详解
形参和实参是什么?
简单来说,形式参数(形参)就是形式上的参数,没有确定值而实际参数(实参)是实际存在的,已经确定的参数,常量,变量,表达式,都是实参。最简单的例子:
形参和实参的区别
实参的值不随形参的变化而变化,实参传递给形参的参数个数类型和顺序都应相同,否则会系统强制转换,出现数据丢失或者“类型不匹配”的错误。
如果实参是数组名,那么形参传递的之就是地址的值。在C里面是无法做到形参改变 实参值同步改变的。如果要用函数改变实参的值,可以用指针作为参数来改变。
在C++中,当按值传递形参时,即使实参的类型带有const,传递给形参时也会被当作不带const的类型来处理。这是因为按值传递时会对实参进行拷贝,而拷贝后的值存储在形参中,形参本身并不会继承const属性。
1.6 字符串常量与字符常量
“A”是字符串常量,‘A’是字符常量
用双引号括起来的是字符串常量,用单引号括起来的是字符常量
1.7 全局变量和局部变量的区别
作用域 声明 生命周期 存储区别
全局变量:
• 在函数外部声明的变量,整个程序都可以访问。
• 声明时会被默认初始化,可以在任何函数中使用。
• 生命周期长,整个程序执行期间都存在。
• 全局变量存储在全局数据区(data)中
局部变量:
• 在函数内部或代码块内部声明的变量,只能在所属的函数或代码块中访问。
• 声明时没有默认初始化,需要手动赋值才能使用。
• 生命周期短,只在所属的函数或代码块的执行期间存在。
• 局部变量存储在栈区(stack)
在C语言中,局部变量的作用域仅限于定义它们的函数或代码块。在函数结束或代码块退出后,局部变量的存储空间会被释放。因此,试图通过指针操作实现对局部变量的全局访问是错误的,因为这样做可能导致未定义行为。
局部变量的地址只能在它们的作用域和生命周期内安全访问。一旦超出作用域或生命周期,访问这些地址可能会导致程序崩溃或其他未定义行为。
因此,以下描述是正确的:
在C语言中,不能通过指针操作实现对局部变量的全局访问。
1.8 int main(int argc, char ** argv)主函数参数说明
在 C 语言中,int main(int argc, char **argv)
是程序的入口点,接受两个参数 argc
和 argv
。这两个参数允许程序从命令行接收输入,使得程序能够根据用户输入进行不同的操作。
argc
(Argument Count)
- 类型:
int
- 功能:
argc
表示命令行参数的个数,包括程序本身的名称。它告诉程序从命令行传递给程序的参数的数量。 - 最小值:
argc
的值至少为 1,因为argv[0]
始终指向程序的名称或路径。
argv
(Argument Vector)
- 类型:char **argv,这是一个字符指针的数组,每个元素都是一个指向字符串的指针,字符串以 null 字符 (\0) 结束。
- 功能:argv 用来存储从命令行传递的参数。每个数组元素 argv[i] 指向一个以 null 结尾的字符串,表示一个命令行参数。
- argv[0]:指向程序的名称或路径。
- argv[1]:指向第一个用户输入的参数,以此类推,argv[argc-1] 指向最后一个命令行参数。
命令行示例
假设我们在命令行中执行如下命令:
在这种情况下,argc
和 argv
的值如下所示:
argc = 4
,表示命令行有 4 个参数。argv
数组中的元素:argv[0] = "./program":程序的名称(或路径)。- argv[1] = "arg1":第一个参数。
- argv[2] = "arg2":第二个参数。
- argv[3] = "arg3":第三个参数。
- argv[4] = NULL:argv 数组的最后一个元素指向 NULL,表示参数的结束。
1.9 static关键字
在 C 中的用法
修饰局部变量(静态局部变量)
在 C 中,static
修饰局部变量时,会改变变量的生命周期,使得该变量不再随着函数调用的结束而销毁。它会在程序运行期间只初始化一次,并保留其值。静态局部变量的生命周期是整个程序的生命周期,但它的作用范围仅限于函数内部。
count
是一个静态局部变量,它在函数countCalls()
第一次被调用时初始化,并在后续的调用中保留其值。- 每次调用该函数时,
count
的值会继续增加。
修饰全局变量和函数
当 static
修饰全局变量和函数时,它会改变它们的链接属性。默认情况下,C 中的全局变量和函数具有外部链接,意味着它们可以在程序的其他文件中访问。而使用 static
修饰后,它们的作用域限制在定义它们的文件内,实现了封装。
globalVar
和staticFunction
都是静态的,意味着它们只能在定义它们的文件中访问,其他文件无法访问它们。- 这种做法通常用于限制某些功能或数据只在特定文件中使用,提高封装性。
在 C++ 中的用法
在 C++ 中,static
关键字除了可以修饰局部变量和全局变量、函数外,还用于静态成员变量和静态成员函数。
静态成员变量
静态成员变量属于类本身,而不是类的具体实例。所有类对象共享同一个静态成员变量,静态成员变量不随对象的创建和销毁而改变。
静态成员变量基本都必须在类外部定义(除了内联静态成员和C++17引入的constexpr静态成员变量)。
count
是一个静态成员变量,它对于所有MyClass
的对象是共享的。每次创建一个MyClass
对象,count
的值就增加。- 静态成员变量
count
在类外定义时需要加上MyClass::
,并初始化为0
。
静态成员函数
静态成员函数是属于类本身的,而不是类的某个具体实例。静态成员函数只能访问静态成员变量和静态成员函数,不能访问非静态的成员。
showCount()
是一个静态成员函数,它可以通过类名调用,也可以通过类的对象调用。- 静态成员函数只能访问静态成员变量
count
,而不能访问类的非静态成员。
总结
- 在 C 中,
static
关键字用于控制变量的存储方式,修饰局部变量时使其保持其状态,修饰全局变量和函数时限制它们的作用范围。 - 在 C++ 中,
static
不仅用于局部变量和全局变量,还用于静态成员变量和静态成员函数,静态成员是类所有对象共享的,且不依赖于具体的类实例。
1.10 typedef和define有什么区别?
typedef与define都是替一个对象取一个别名,以此来增强程序的可读性,但是它们在使用和作用上也存在着以下4个方面的不同。
原理不同
#define是C语言中定义的语法,它是预处理指令,在预处理时进行简单而机械的字符串替换,不做正确性检査。
typedef是关键字,它在编译时处理,所以 typedef具有类型检查的功能。
功能不同
typedef用来定义类型的别名,这些类型不仅包含内部类型(int、char等),还包括自定义类型(如 struct),可以起到使类型易于记忆的功能。
#define不只是可以为类型取别名,还可以定义常量、变量、编译开关等。
作用域不同
#define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而 typedef有自己的作用域。
#嵌入式笔面经分享##嵌入式转岗的难度怎么样##嵌入式##嵌入式八股#作者简介:仅用几个月时间0基础天坑急转嵌入式开发,逆袭成功拿下华为、vivo、小米等15个offer,面试经验100+,收藏20+面经,分享求职历程与学习心得。 专栏内容:这是一份覆盖嵌入式求职过程中99%问题指南,详细讲解了嵌入式开发的学习路径、项目经验分享、简历优化技巧、面试心得及实习经验,从技术面,HR面,AI面,主管面,谈薪一站式服务,助你突破技术瓶颈、打破信息差,争取更多大厂offer。