文章目录
- C的指针和C++引用
- 使用 `*`(指针)
- 特点
- 使用场景
- C++引用传递
- 传递机制的差异
- 内存安全性
- 5. **是否可以为NULL**
- 常量指针/常量引用
- C的常量指针传递
- C++的常量引用传递
- 9. **总结对比**
- URL
- URL 的结构
- 协议(Scheme)
- 域名或 IP 地址
- 端口(Port)
- 路径(Path)
- 查询参数(Query Parameters)
- 片段标识符(Fragment)
- URL 示例解析
- 示例 1
- 函数指针和指针函数
- 1. 函数指针 (Function Pointer)
- 语法:
- 例子:
- 2. 指针函数 (Pointer Function)
- 语法:
- 例子:
- 3. 区别总结
- 4. 结合例子理解
- sizeof和strlen
- 数组指针和指针数组
- 数组指针(Array Pointer to Array)
- 指针数组(Array of Pointers)
- 使用场景
- 内存分配
- 结构体和联合体
- 结合体(Structure)
- 联合体(Union)
- 内存对齐(Alignment)
- 示例
- 野指针
- 可能导致野指针的情形:
- 如何避免野指针:
- 数组和链表
- 写个宏返回较小值
- 注意事项
- define 和typedef
- `#define`
- `typedef`
- static的作用
- 内存泄漏
- 定义
- 原因
- 解决方法
- 数组名和指针
- 指针常量、常量指针和指向常量的常量指针
- 堆和栈
- malloc和new
- struct和class
- 默认访问权限
- 继承访问权限
- 语义区别
- 示例
- 内联函数
- 如何声明内联函数:
- 注意事项:
- 分区
- 编译过程
- SPI和IIC的寻址
- SPI寻址
- IIC寻址
- 交叉编译
- 交叉编译的工具:
- 交叉编译的应用场景:
- SPI可以去掉哪些线
C的指针和C++引用
使用 *
(指针)
在函数定义中,使用*
表示通过指针传递数据地址,允许函数访问和修改外部变量。
特点
- 传递的是变量的地址(指针值),即通过传递地址来访问实际数据。
- 可以修改调用者提供的变量值。
- 需要显式解引用(
*
)来访问指针指向的数据。
使用场景
-
需要修改外部变量的值:
函数通过传递变量的地址,直接操作外部变量的值。例如:void updateValue(int *x) {*x = 42; // 修改指针指向的变量的值 }int main() {int a = 0;updateValue(&a); // 传入变量的地址printf("a = %d\n", a); // 输出 a = 42return 0; }
-
传递大型数据结构:
避免拷贝大数据结构带来的性能开销,只传递数据地址。例如:void processArray(int *arr, int size) {for (int i = 0; i < size; i++) {arr[i] += 1; // 修改数组内容} }
C++引用传递
在C++中,通过引用传递参数时,使用&
来声明引用。引用本质上是一个别名,它直接指向传入的对象。
C++引用传递函数示例:
#include <iostream>
using namespace std;void modifyValue(int& x) {x = 100; // 修改x的值,直接操作引用
}int main() {int a = 10;cout << "Before: a = " << a << endl;modifyValue(a); // 传递a的引用cout << "After: a = " << a << endl;return 0;
}
传递机制的差异
- C指针传递: 使用指针传递时,函数接受的是指针的副本,指针指向的内存地址可以直接修改。函数通过
*
操作符来访问和修改指针指向的内存。 - C++引用传递: 引用传递时,函数接受的是原始对象的别名,引用本身就指向原始变量。引用的使用就像直接操作变量一样,语法上没有指针解引用的麻烦。
内存安全性
- C指针传递: 由于指针可以为空(NULL),在使用指针时可能出现悬空指针、空指针等问题,增加了程序出错的风险。
- C++引用传递: 引用在声明时必须初始化,并且不能为NULL,因此在大多数情况下,引用使用起来比指针更安全。如果你需要一个非空的引用,引用是比指针更合适的选择。
5. 是否可以为NULL
- C指针传递: 可以传递NULL指针,这可能导致运行时错误。函数内部需要检查指针是否为NULL,避免空指针解引用。
void modifyValue(int* x) {if (x != NULL) {*x = 100;}
}
- C++引用传递: 引用永远不能为NULL,如果你尝试将引用绑定到NULL,编译器会报错。
void modifyValue(int& x) {x = 100;
}// 不能做以下操作,编译时会报错
// modifyValue(NULL);
常量指针/常量引用
在C和C++中,都可以传递常量指针或常量引用来避免修改传入的参数。
C的常量指针传递
void printValue(const int* x) {printf("%d\n", *x); // 只读指针,不能修改指针指向的值
}
C++的常量引用传递
void printValue(const int& x) {cout << x << endl; // 只读引用,不能修改x的值
}
常量引用和常量指针的区别在于,常量引用简化了语法,避免了使用*
进行解引用,同时也避免了指针可能为NULL的情况。
9. 总结对比
特性 | C 指针传递 | C++ 引用传递 |
---|---|---|
语法 | * 解引用传递地址 | 直接操作引用 |
初始化 | 可以是NULL | 必须初始化,不能是NULL |
是否可以修改 | 可以修改指针指向的对象 | 直接修改原始对象 |
空指针检查 | 需要显式检查指针是否为NULL | 无法为NULL,编译时会报错 |
简洁性 | 语法较为复杂,需要解引用和指针操作符 | 更简洁,像直接操作变量一样 |
内存安全性 | 存在空指针、悬空指针等风险 | 更安全,引用永远指向有效对象 |
常量传递 | const int* x | const int& x |
URL
URL(Uniform Resource Locator)是统一资源定位符,是用于标识互联网上资源的字符串。它是网络资源(如网页、图片、视频等)的地址,用户可以通过浏览器输入 URL 访问对应的内容。
URL 的结构
URL 通常由以下部分组成:
<协议>://<域名或IP地址>:<端口>/<路径>?<查询参数>#<片段标识符>
协议(Scheme)
协议指明了资源的访问方式,例如:
http
: 超文本传输协议https
: 安全的超文本传输协议ftp
: 文件传输协议
示例:
https://www.example.com
域名或 IP 地址
这是服务器的地址:
- 域名: 如
www.google.com
- IP 地址: 如
192.168.1.1
示例:
https://www.example.com
端口(Port)
端口是服务器用来区分不同服务的标识,常见端口号:
- HTTP 默认端口是 80
- HTTPS 默认端口是 443
如果使用默认端口,可以省略;否则需要显式指定:
http://www.example.com:8080
路径(Path)
路径指向具体的资源位置,例如文件或目录:
https://www.example.com/images/photo.jpg
查询参数(Query Parameters)
查询参数以 ?
开始,用于传递动态数据,参数之间用 &
分隔:
https://www.example.com/search?q=url&lang=en
q=url
: 参数名为q
,值为url
lang=en
: 参数名为lang
,值为en
片段标识符(Fragment)
片段标识符以 #
开头,用于定位网页中的某一部分:
https://www.example.com/article#section2
这里 #section2
定位到文章的第 2 部分。
URL 示例解析
示例 1
https://www.example.com/products?id=123&category=books#reviews
解析:
- 协议:
https
- 域名:
www.example.com
- 路径:
/products
- 查询参数:
id=123
和category=books
- 片段标识符:
reviews
函数指针和指针函数
函数指针:回调中经常使用函数指针,就是个指向函数的指针;
指针函数:就是一个普通函数,只是返回值是返回某个数据的地址;
1. 函数指针 (Function Pointer)
语法:
返回类型 (*指针变量名)(参数类型);
例子:
#include <stdio.h>// 定义一个普通函数
int add(int a, int b) {return a + b;
}// 定义一个函数指针类型
typedef int (*AddPointer)(int, int);int main() {// 声明并初始化一个函数指针AddPointer ptr = add;// 通过函数指针调用函数int result = ptr(3, 4);printf("Result: %d\n", result); // 输出: Result: 7return 0;
}
在上面的例子中,AddPointer
是一个函数指针类型,指向返回类型为 int
,参数为两个 int
类型的函数。我们通过 ptr
来调用 add
函数。
“*”的优先级低于“()”的优先级
2. 指针函数 (Pointer Function)
指针函数是一个返回指针的函数。它的返回类型是一个指针,可以指向任何类型的数据。
语法:
返回类型* 函数名(参数类型);
例子:
#include <stdio.h>// 定义一个返回指针的函数
int* findMax(int* a, int* b) {if (*a > *b) {return a;} else {return b;}
}int main() {int x = 5, y = 10;// 调用返回指针的函数int* max = findMax(&x, &y);printf("Max: %d\n", *max); // 输出: Max: 10return 0;
}
在上面的例子中,findMax
是一个指针函数,它返回一个指向整数的指针。在 main
函数中,我们调用 findMax
来获取 x
和 y
中的最大值,并通过指针返回该值。
3. 区别总结
特性 | 函数指针 (Function Pointer) | 指针函数 (Pointer Function) |
---|---|---|
定义 | 指向函数的指针,用于调用函数。 | 返回指针的函数,用于返回数据的地址。 |
语法 | 返回类型 (*指针名)(参数类型); | 返回类型* 函数名(参数类型); |
目的 | 允许通过指针调用函数,实现动态调用等功能。 | 返回指向某个数据的指针,常用于处理大数据或动态内存分配。 |
例子 | int (*ptr)(int, int) = add; | int* findMax(int* a, int* b) |
4. 结合例子理解
#include <stdio.h>// 1. 定义一个函数,返回两个数的最大值
int* findMax(int* a, int* b) {if (*a > *b) {return a;} else {return b;}
}// 2. 定义一个函数指针类型,指向返回类型为int,参数类型为int*的函数
typedef int* (*MaxPointer)(int*, int*);int main() {int x = 5, y = 10;// 使用指针函数int* max = findMax(&x, &y);printf("Max: %d\n", *max); // 输出: Max: 10// 使用函数指针MaxPointer ptr = findMax; // 将findMax函数赋值给指针max = ptr(&x, &y); // 使用指针调用函数printf("Max: %d\n", *max); // 输出: Max: 10return 0;
}
在这个例子中:
findMax
是一个指针函数,返回一个指向整数的指针。MaxPointer
是一个函数指针类型,指向返回类型为int*
的函数,我们通过函数指针ptr
来调用findMax
。
sizeof和strlen
strlen(“\0”) =0,sizeof(“\0”)=2。
sizeof既是编译时运算符又是关键字,sizeof后如果是类型,则必须加括弧,如果是变量名,则可以不加括弧,后面还可以是函数,输出结果为返回值。
strlen是运行时函数在运行期计算的,用来计算字符串的实际长度,不是类型占内存的大小,只能用于 char*
类型的参数。
例如, char str[20] = "0123456789”,字符数组str是编译期大小已经固定的数组,在32位机器下,为sizeof(char)*20=20,而其 strlen大小则是在运行期确定的,所以其值为字符串的实际长度10。
数组指针和指针数组
数组指针(Array Pointer to Array)
数组指针是一个指向数组首元素的指针变量。通过数组指针,你可以访问数组中的单个元素或者整个数组。本质上花里胡哨的就是个指针。
int array[] = {10, 20, 30};
int(*ptr) = array; // ptr 是指向数组 array 的指针
cout << *ptr << endl; // 输出 10,即数组的第一个元素
指针数组(Array of Pointers)
指针数组是一个数组,其元素都是指针。这种结构可以用来创建动态二维数组或者在需要指针的场合使用。
int a = 10, b = 20, c = 30;
int* ptrArray[] = {&a, &b, &c}; // ptrArray 是一个包含三个整型指针的数组
cout << ptrArray[0] << endl; // 输出 10,即变量 a 的值
使用场景
-
数组指针:
- 用于通过指针访问和操作数组元素。
- 作为函数参数传递数组时,可以避免数组的复制,提高效率。
- 用于动态内存分配,如
new int[size]
。
-
指针数组:
- 用于创建动态二维数组。
- 用于存储多个动态分配的内存块的地址。
- 用于实现复杂的数据结构,如链表、树等。
内存分配
-
静态存储器分配(Static Storage):
- 用于全局变量和静态变量。这些变量在程序开始执行之前分配内存,并在程序结束时释放。它们的生命周期贯穿整个程序执行过程。
- 静态分配的内存在所有函数之外,它们在任何函数调用之前就存在,并在程序结束时销毁。
- 静态分配的内存通常用于那些需要在程序的整个生命周期内持续访问的数据。
-
栈上分配(Stack Allocation):
- 发生在函数内部的局部变量是在栈上分配的。当函数被调用时,局部变量被创建,当函数执行完毕后,这些变量被销毁。
- 栈上分配是快速的,因为栈内存的分配和释放是自动管理的,不需要显式的
free
或delete
语句。 - 栈上分配的内存大小有限,并且不适合大型数据结构或需要长时间存活的数据。
-
堆上分配(Heap Allocation):
- 使用
malloc
、new
或其他内存分配函数在堆上动态分配内存。堆内存在程序的任何地方都可以分配,并且必须在适当的时候手动释放。 - 堆内存的生命周期不由其创建的位置决定,而是由程序员控制的。如果未正确管理,可能导致内存泄漏。
- 堆内存分配适用于那些大小在编译时未知或需要在程序运行时动态改变的数据结构。
- 在 C 中,使用
malloc
和free
来分配和释放堆内存。在 C++ 中,可以使用new
和delete
操作符来分配和释放堆内存。
- 使用
结构体和联合体
结合体(Structure)
- 内存分配:当创建一个结构体变量时,为其所有成员分配连续的内存空间。结构体的大小是其所有成员大小的总和,对齐到最大的成员类型大小。
- 访问:结构体的成员通过点操作符
.
访问,如structExample var; var.a = 10;
。
联合体(Union)
- 定义:使用
union
关键字定义,可以包含多个不同类型的成员,但是在任意时刻只能存储其中一个成员的值。 - 内存分配:联合体只为其最大成员分配足够的内存空间。这意味着联合体的大小是其所有成员中最大的那个的大小。
- 访问:联合体的成员同样通过点操作符
.
访问,但必须注意同时只能有一个成员包含有效的数据。
内存对齐(Alignment)
在内存分配时,编译器会考虑类型的大小和对齐要求。对齐可以提高内存访问的效率,但可能会导致内存的浪费。例如,如果一个结构体包含一个 int
(通常是 4 字节)和一个 char
(1 字节),结构体的大小可能会被扩展到 8 字节以满足对齐要求。
示例
struct ExampleStruct {int a; // 4 byteschar b; // 1 bytedouble c; // 8 bytes
};union ExampleUnion {int a; // 4 byteschar b; // 1 bytedouble c; // 8 bytes
};
在这个例子中,ExampleStruct
的大小至少是 16 字节,8字节对齐(4+4)+8,以满足 double
类型的对齐要求。而 ExampleUnion
的大小是 8 字节,因为联合体只为最大成员 double
分配空间。
野指针
在 C 和 C++ 中,野指针(Wild Pointer)是指指向未知或非法内存位置的指针。野指针的存在通常是由于指针操作不当或未初始化的指针。野指针的使用可能导致程序崩溃、数据损坏或安全漏洞。
可能导致野指针的情形:
-
未初始化的指针:指针变量声明后未被赋予有效的内存地址。
int* ptr;// ptr 未初始化,是一个野指针
-
内存释放后继续使用:动态分配的内存在使用
free
或delete
后,没有把指针赋值为nullptr,继续使用该指针。int* ptr = new int(10);delete ptr;// ptr 现在是野指针,因为所指向的内存已经被释放
-
越界访问:访问数组或容器时超出其边界。
int arr[5]; int* out_of_bounds = &arr[10]; // 越界访问,out_of_bounds 是野指针
-
函数返回局部变量的地址:函数返回局部变量的地址。
int* func() {int var = 10;return &var; // 错误,返回的是局部变量的地址}
-
错误的类型转换:将一个类型的指针赋值给另一个不兼容类型的指针。
char* char_ptr; int* int_ptr = char_ptr; // 错误的类型转换
如何避免野指针:
-
初始化指针:确保所有指针在使用前都被初始化为
nullptr
或指向有效的内存地址。int* ptr = nullptr;
-
检查内存分配:使用动态内存分配时,检查分配是否成功。
int* ptr = new int(10);if (ptr == nullptr) {// 处理分配失败的情况}
-
避免越界访问:在访问数组或容器时,确保索引在有效范围内。
-
避免返回局部变量的地址:不要从函数返回局部变量的地址,可以使用动态内存分配或返回值的副本。
-
使用智能指针:C++11 引入了智能指针,如
std::unique_ptr
和std::shared_ptr
,它们可以自动管理内存,减少野指针的风险。 -
使用工具:利用编译器的警告和运行时检查工具(如 Valgrind、AddressSanitizer)来检测野指针的使用。
数组和链表
特性 | 数组(Array) | 链表(Linked List) |
---|---|---|
存储方式 | 内存中连续存储 | 元素通过指针链接,非连续存储 |
访问速度 | 通过索引直接访问,O(1) | 从头或尾开始遍历,O(n) |
内存使用 | 需要预先分配固定大小的内存 | 动态增长和收缩,节省内存 |
插入/删除 | 可能需要移动元素以维护连续性,O(n) | 改变指针,通常O(1) |
适用场景 | 元素数量固定或变化不大,频繁随机访问 | 元素数量动态变化,插入删除频繁 |
实现方式 | 动态分配内存(如使用 new[] 或 malloc ) | 结构体和指针实现,每个节点含数据和指针 |
优缺点 | 访问快,内存不够灵活,插入删除效率低 | 内存灵活,插入删除快,访问慢,需要指针空间 |
写个宏返回较小值
#define MIN(a, b) ((a) < (b) ? (a) : (b))// 使用宏的示例
int main() {int x = 10;int y = 5;int smaller = MIN(x, y); // 这将返回 x 和 y 中的较小值,即 5return 0;
}
在这个宏定义中,MIN
接收两个参数 a
和 b
。表达式 (a) < (b) ? (a) : (b)
是一个三元运算符,它检查 a
是否小于 b
。如果是,它返回 a
,否则返回 b
。
注意事项
宏与函数:虽然宏在很多情况下可以替代函数,但宏没有类型检查,可能会导致运行时错误。如果参数类型不确定,或者宏的逻辑较为复杂,考虑使用内联函数可能更安全。所以必须要给a,b加括号,先计算括号里的类型
在 C 和 C++ 中,#define
和 typedef
是两种用于定义符号或别名的预处理器指令,它们在编译时处理,具有不同的用途和特点。
define 和typedef
#define
#define
是一个预处理器指令,用于创建宏。宏可以接受参数,也可以不接受参数。宏在预处理阶段被文本替换,这意味着在编译之前,宏的名称将被其定义的代码替换。不会进行正确性检查
用途:
- 创建常量:
#define PI 3.14159265358979323846
- 创建函数样的宏:
#define MAX(a, b) ((a) > (b) ? (a) : (b))
注意:
- 宏没有类型检查,这可能导致类型不匹配的错误。
- 宏替换是文本替换,可能导致意外的副作用,尤其是在使用复杂表达式作为参数时。
typedef
typedef
是一个关键字,用于为类型创建别名。它允许你为现有类型创建一个新的名称,这有助于提高代码的可读性和可维护性。
用途:
- 创建类型的别名:
typedef unsigned int uint;
- 创建结构体、联合体或枚举类型的别名:
typedef struct { int x, int y; } Point;
注意:
typedef
定义的是类型别名,它在编译时处理,与原始类型具有相同的类型安全和特性。typedef
可以用于简化复杂的类型声明,使其更易于理解和使用。
static的作用
只会被初始化一次,通过在变量或函数前加上static关键字,可以限制其作用范围在本文件内,从而避免命名冲突,并实现信息的隐藏。
内存泄漏
定义
内存泄漏是指程序中已动态分配的堆内存由于某种原因未被释放或无法释放,从而导致系统内存浪费的现象。
原因
内存泄漏是一种常见的计算机编程错误,特别是在使用诸如C、C++等需要手动管理内存的语言时更为常见。当一个程序在运行期间动态地申请了内存(如通过malloc、calloc、realloc或new操作),但在不再需要这块内存时未能正确地释放它(通过free或delete操作),就会导致内存泄漏[1]。这种未释放的内存会一直占用系统资源,直到程序结束运行。
解决方法
内存泄漏的检测和修复是软件开发过程中的重要环节。开发者可以使用各种工具和方法来检测内存泄漏,例如Valgrind、AddressSanitizer、Visual Studio的内置内存泄漏检测工具等。此外,采用良好的编程习惯,如使用智能指针、遵循资源获取即初始化(RAII)原则、定期进行代码审查和使用自动化测试,也能有效预防内存泄漏的发生。
数组名和指针
特性 | 数组名 | 指针 |
---|---|---|
定义和用途 | 数组名代表整个数组,是常量指针,指向数组的第一个元素。 | 指针是一个变量,存储另一个变量的内存地址,可以重新赋值。 |
内存分配 | 编译时分配固定大小的内存。 | 指针变量本身分配内存,指向的内存可以动态分配和释放。 |
可变性 | 数组名不可变,总是指向数组的首地址。 | 指针变量的值可以改变,可以指向不同的内存地址。 |
运算 | 数组名加上索引得到元素,例如 arr[3] 。 | 指针加上整数移动指针位置,例如 ptr + 3 。 |
传递给函数 | 实际传递的是指向数组第一个元素的指针。 | 传递的是指针变量的值,即它所指向的地址。 |
数组名作为参数 | 在函数参数中被解释为指向数组第一个元素的指针。 | 直接传递指针变量。 |
类型 | 数组类型[] ,例如 int[] 。 | 指向的类型* ,例如 int* 。 |
数组名的退化 | 在某些表达式中,数组名会退化为指向数组第一个元素的指针。 | 不存在退化,指针变量就是指针。 |
指针常量、常量指针和指向常量的常量指针
-
指针常量(Pointer to Constant):
- 指针常量是指指针变量的值不可变,即一旦指针被初始化后,它的值(即它所指向的地址)不能改变。
- 但是,指针常量指向的内存区域的内容是可以修改的。
- 声明方式:
int a = 10; int *const ptr = &a;
这里ptr
是一个指针常量,它始终指向a
,但不能指向其他地址。
-
常量指针(Constant Pointer):
- 常量指针是指指针指向的是一个常量值,即指针指向的值不能被修改。函数指针是指向函数的指针,数组指针是指向数组的指针
- 但是,常量指针本身的值(即它所指向的地址)是可以改变的。
- 声明方式:
const int a = 10; int *ptr = &a;
这里ptr
是一个常量指针,它指向的a
的值不能被修改,但ptr
可以指向其他地址。
-
指向常量的常量指针(Constant Pointer to Constant):
- 指向常量的常量指针是指指针本身的值不可变,并且指针指向的值也不能被修改。
- 声明方式:
const int a = 10; const int *const ptr = &a;
这里ptr
是一个指向常量的常量指针,它始终指向a
,并且a
的值也不能被修改。
下面是这三个概念的表格总结:
概念 | 指针变量的值是否可变 | 指针指向的值是否可变 | 声明方式 |
---|---|---|---|
指针常量(Pointer to Constant) | 不可变(常量) | 可变 | int a = 10; int *const ptr = &a; |
常量指针(Constant Pointer) | 可变 | 不可变(常量) | const int a = 10; int *ptr = &a; |
指向常量的常量指针(Constant Pointer to Constant) | 不可变(常量) | 不可变(常量) | const int a = 10; const int *const ptr = &a; |
int const *p1; const 在前,定义为常量指针
int *const p2; * 在前,定义为指针常量
const在号前,就修饰这个值是常量,表示这个值不能修改;
const在号后,就修饰这个地址是常量,表示这个地址不能修改;
堆和栈
特性 | 堆(Heap) | 栈(Stack) |
---|---|---|
内存分配方式 | 手动管理,使用malloc 、new 等分配,free 、delete 释放。 | 自动管理,函数调用时分配,函数返回时释放。 |
内存大小 | 通常更大,受限于虚拟内存。 | 大小有限,通常较小,由系统预先定义。 |
内存访问速度 | 较慢,因为内存分配不连续。 | 较快,因为是连续的内存区域。 |
数据存储 | 用于存储动态分配的数据,如通过malloc 或new 分配的对象。 | 用于存储局部变量和函数参数。 |
生命周期 | 由程序员控制,直到显式释放内存。 | 与创建它的函数调用相同,函数返回时数据被销毁。 |
内存碎片 | 容易产生内存碎片。 | 不会产生内存碎片,因为是连续的。 |
内存越界 | 可能导致数据损坏,但不一定立即导致程序崩溃。 | 可能导致程序崩溃,因为栈空间有限,越界可能覆盖重要数据。 |
跨函数调用 | 可以跨函数调用访问。 | 通常不能跨函数调用访问,除非通过指针或引用。 |
malloc和new
在C和C++中,malloc
和new
都是用于动态内存分配的函数,但它们有一些关键的区别:
特性 | malloc (C语言) | new (C++) |
---|---|---|
语言支持 | 仅在C语言中可用。 | 仅在C++中可用。 |
语法 | void* malloc(size_t size); | T* new(size_t size); 或 new T; |
返回类型 | 返回一个void* 类型的指针,需要显式转换为需要的类型。 | 返回一个指向新分配对象的指针,类型与申请的对象类型相同。 |
构造函数 | 不调用构造函数,只分配内存。 | 自动调用对象的构造函数。 |
析构函数 | 不自动调用析构函数。 | 自动调用对象的析构函数。 |
错误处理 | 如果内存分配失败,返回NULL 。 | 如果内存分配失败,抛出std::bad_alloc 异常。 |
内存对齐 | 不保证对象的内存对齐。 | 保证对象的内存对齐。 |
分配大小 | 需要指定分配的字节大小。 | 对于单个对象,不需要指定大小,编译器会自动计算。 |
数组分配 | 对于数组分配,需要手动在类型转换后添加元素计数。 | 可以使用new[] 和delete[] 来分配和释放数组。 |
释放内存 | 使用free(void* ptr); 释放内存。 | 使用delete 或delete[] 释放内存。 |
总结来说,malloc
是C语言中的内存分配函数,它只分配内存而不进行任何构造或析构操作。new
是C++中的运算符,它不仅分配内存,还调用对象的构造函数来初始化对象,并在对象生命周期结束时自动调用析构函数。在C++中,推荐使用new
和delete
来分配和释放对象,因为它们支持C++的构造和析构机制,而malloc
和free
通常用于与C代码库交互或者在需要手动管理内存大小时使用。
struct和class
在C++中,struct
和class
都可以用来定义自定义数据类型,它们在很多方面是相似的,但在默认访问权限和继承方面有一些区别。以下是struct
和class
的主要区别:
特性 | struct | class |
---|---|---|
默认访问权限 | public | private |
继承访问权限 | 公有继承(public inheritance) | 私有继承(private inheritance) |
语义区别 | 通常用于简单的数据结构,强调数据的聚合。 | 通常用于定义对象,强调数据和方法的封装。 |
默认访问权限
struct
:在struct
中定义的成员默认是public
的,这意味着它们可以被任何访问struct
实例的代码访问和修改。class
:在class
中定义的成员默认是private
的,这意味着它们只能被类的内部成员函数和友元函数访问和修改。
继承访问权限
struct
:当一个struct
继承另一个struct
或class
时,默认使用公有继承(public inheritance),这意味着基类的公有成员和保护成员会成为派生类的公有成员和保护成员。class
:当一个class
继承另一个class
或struct
时,默认使用私有继承(private inheritance),这意味着基类的公有成员和保护成员会成为派生类的私有成员和保护成员。
语义区别
struct
:struct
最初被设计为用于定义简单的数据结构,类似于C语言中的结构体。在C++中,struct
可以包含数据成员和成员函数,但它通常被用来表示没有太多封装和复杂行为的聚合数据。class
:class
被设计为面向对象编程的基本构建块,用于定义具有封装、继承和多态特性的对象。class
强调的是数据和方法的封装,以及对象的行为。
示例
struct Data {int value; // 默认public
};class Object {int value; // 默认private
public:void set(int v) { value = v; }int get() const { return value; }
};Data d = { 10 }; // 直接访问
Object o;
o.set(10); // 通过公共接口访问
内联函数
在C++中,内联函数是一种特殊的函数,它建议编译器在每次调用该函数的地方将其代码直接插入(即“内联”),而不是进行常规的函数调用。这样做的目的是为了减少函数调用的开销,尤其是在函数体较小的情况下,因为函数调用涉及到一系列的操作,如保存寄存器、堆栈操作等,这些操作对于小函数来说可能比函数本身的执行时间还要长。
如何声明内联函数:
在C++中,可以使用inline
关键字来声明一个内联函数。这个关键字可以放在函数定义之前,也可以放在函数声明之前,或者两者都放。
inline int add(int a, int b) {return a + b;
}
或者
inline int multiply(int a, int b);
int multiply(int a, int b) {return a * b;
}
注意事项:
- 编译器自由:
inline
只是一个对编译器的建议,编译器可以选择忽略这个建议。 - 模板函数:模板函数默认就是内联的,因为它们需要在编译时实例化,所以不需要显式声明为
inline
。 - 虚函数:虚函数不能是内联的,因为它们在运行时需要进行动态绑定,这与内联的静态绑定特性相冲突。
- 多文件定义:如果一个内联函数在多个文件中定义,那么这些定义必须完全相同,否则会导致链接错误。
分区
代码段、数据段、BSS段、堆和栈是程序在内存中的不同区域,每个区域都有特定的用途和特性。下面是这些内存区域的详细解释:
-
代码段(Text Segment):
- 存放程序的执行代码,这部分区域的大小在程序运行前就已经确定。
- 通常这块内存区域属于只读,但有些架构也允许可写。
- 代码段中也可能包含只读的常数变量,例如字符串常量等。
- 程序代码在内存中映射,一个程序可以在内存中有多个副本。
-
数据段(Data Segment):
- 存放程序中已初始化的全局变量和已初始化为非0的静态变量。
- 属于静态内存分配,即在程序编译时分配。
- 直观理解就是C语言程序中的全局变量。
-
BSS段(BSS Segment):
- 存放程序中未初始化的全局变量和未初始化的静态变量,或者初始化为0的静态变量。
- BSS是“Block Started by Symbol”的缩写,属于静态内存分配。
- BSS段的特点是被初始化为0。
- BSS段本质上也是属于数据段,可以看作是被初始化为0的数据段。
4.1 数据段(.data)和BSS段的区别和联系:
- 二者本来没有本质区别,都是用来存放C程序中的全局变量。
- 区别在于把显式初始化为非零的全局变量存放在.data段中,而把显式初始化为0或者未显式初始化(C语言规定未显式初始化的全局变量值默认为0)的全局变量存放在BSS段。
-
堆(Heap):
- 用来存放进程中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。
- 当进程调用malloc等函数分配内存时,新分配的内存就被动态分配到堆上。
- 当利用free等函数释放内存时,被释放的内存从堆中被剔除。
-
栈(Stack):
- 用来存放程序中的局部变量(包括函数参数和局部变量)以及返回地址等信息。
- 栈的内存分配是自动的,遵循后进先出(LIFO)的原则。
- 栈的大小通常比堆小,且有最大限制。
示例:
char *p1 = (char *)malloc(10);
char *p2 = "123456";
char *p3 = "123456";
strcpy(p1, "123456"); // "123456\0" 放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
free(p1);
return 0;
p1
是通过malloc
动态分配的内存,存储在堆上。p2
和p3
指向字符串常量 “123456”,这些字符串常量通常存储在代码段的只读数据区域。strcpy(p1, "123456")
将字符串 “123456” 复制到p1
指向的堆内存中。free(p1)
释放之前通过malloc
分配的内存。
编译过程
将.c
文件(C语言源文件)转换为可执行文件的过程通常涉及以下几个步骤:
-
预处理(Preprocessing):
- 使用预处理器(通常由编译器驱动程序如
gcc
或clang
调用)处理源代码文件中的预处理指令,如宏定义的展开、条件编译指令、包含头文件等。 - 命令示例:
gcc -E hello.c -o hello.i
(这一步通常由编译器自动完成,不是必须的独立步骤)
- 使用预处理器(通常由编译器驱动程序如
-
编译(Compilation):
- 编译器将预处理后的源代码转换成汇编语言代码。
- 命令示例:
gcc -S hello.i -o hello.s
-
汇编(Assembly):
- 汇编器将汇编语言代码转换成机器码,生成目标文件(通常是
.o
文件)。 - 命令示例:
gcc -c hello.s -o hello.o
- 汇编器将汇编语言代码转换成机器码,生成目标文件(通常是
-
链接(Linking):
- 链接器将一个或多个目标文件与库文件、其他资源链接在一起,生成最终的可执行文件。
- 命令示例:
gcc hello.o -o hello
SPI和IIC的寻址
SPI寻址
- 片选信号(CS):SPI通过向对应从机发送使能信号(片选信号CS)来寻址。每个从设备都需要一个独立的片选线,由主设备控制。
- 全双工通信:SPI支持全双工通信,即数据发送和接收可以同时进行。
- 无应答机制:SPI没有定义通信应答机制,主设备通常不知道指定的从设备是否存在,需要通过其他方式来实现通信控制。
- 时钟极性和相位可配置:SPI的时钟极性(CPOL)和时钟相位(CPHA)可以配置,这影响数据采样的时刻。
IIC寻址
- 总线广播地址:IIC通过向总线广播从机地址来进行寻址,不需要独立的片选线。
- 半双工通信:IIC是半双工通信,即数据发送和接收不能同时进行。
- 应答机制:IIC具有应答机制,主设备可以通过应答信号(ACK)和非应答信号(NACK)与从设备进行通信确认。
- 固定的时钟极性和相位:IIC的时钟极性和相位是固定的,不像SPI那样可以配置。
- 寻址字节:主机在起始信号之后,紧接着就向总线上发送8位(1字节)数据用于寻址。这8位数据中的D7~D1组成从机地址,D0是数据传送方向位(R/W),为0代表主机向从机写数据,为1代表主机由从机读数据。
交叉编译
交叉编译是一种编译过程,它允许开发人员在一个平台上编写代码,并编译生成可在另一个不同平台上运行的程序。这里的平台通常指的是计算机的体系结构(如x86, ARM, MIPS等)和操作系统(如Linux, Windows, macOS等)的组合。
这里需要注意的是所谓平台,实际上包含两个概念:体系结构(Architecture)、操作系统(OperatingSystem)。同一个体系结构可以运行不同的操作系统;同样,同一个操作系统也可以在不同的体系结构上运行。举例来说,我们常说的x86 Linux平台实际上是Intel x86体系结构和Linux for x86操作系统的统称;而x86 WinNT平台实际上是Intel x86体系结构和Windows NT for x86操作系统的简称。
交叉编译的工具:
交叉编译工具链是一个由编译器、连接器和解释器组成的综合开发环境,
交叉编译的应用场景:
有时是因为目的平台上不允许或不能够安装我们所需要的编译器,而我们又需要这个编译器的某些特征;
有时是因为目的平台上的资源贫乏,无法运行我们所需要编译器;
有时又是因为目的平台还没有建立,连操作系统都没有,根本谈不上运行什么编译器。
SPI可以去掉哪些线
SPI(Serial Peripheral Interface)是一种高速的,全双工,同步的通信总线,通常由四根线组成:
- MOSI(Master Out Slave In):主设备数据输出,从设备数据输入线。
- MISO(Master In Slave Out):主设备数据输入,从设备数据输出线。
- SCK(Serial Clock):时钟线,由主设备控制,用于同步数据传输。
- CS(Chip Select):片选线,用于激活特定的从设备。
然而,在某些特殊情况下,出于简化硬件设计或降低成本的目的,某些线可以被省略或替代:
-
CS线:在只有一个从设备的系统中,CS线可以省略,因为不需要选择从设备。但在有多个从设备的情况下,CS线是必须的
-
MISO线:如果主设备只需要向从设备发送数据,而不需要接收从设备的数据,MISO线可以省略。这种单向SPI通信被称为SPI单向发送模式。
-
MOSI线:如果主设备只需要从从设备接收数据,而不需要向从设备发送数据,MOSI线可以省略。这种单向SPI通信被称为SPI单向接收模式。
-
SCK线:在理论上,SCK线是必不可少的,因为它用于同步数据传输。没有SCK线,SPI通信无法进行。但在某些特殊的应用中,可能会使用其他方式来同步数据,例如通过I2S(Inter-IC Sound)协议,它与SPI有相似之处,但在时钟线上有所不同。