自查目录
1. typedef 和 #define 的区别
2. const 、volatile 和 static 的区别
3. const修饰指针
4. 数组指针和指针数组
5. 函数指针和指针函数
6. C/C++内存管理
6.1 内存分布图解
6.2 C语言中的内存分配方式
6.3 堆(Heap)和栈(Stack)的区别
6.4 栈在C语言中的作用
6.5 C++的内存管理
6.6 内存泄漏
6.7 new/delete与malloc/free的区别
7. C语言部分
7.1 # include< filename. h>和#include" filename. h"的区别
7.2 在头文件中定义静态变量?
7.3 C语言宏中“#”和“##”的用法
7.4 struct与 union的区别
7.5 左值和右值
7.6 有符号数和无符号数的运算
7.7 短路求值
7.8 大端和小端
8. 更多C语言补充内容...
1. typedef 和 #define 的区别
typedef与#define都是替一个对象取一个别名,以此来增强程序的可读性。
#define 它是预处理指令,在预处理时进行简单的字符串替换,不做正确性检査,不管含义是否正确照样代入。只有在编译已被展开的源程序时,才会发现可能的错误并报错。
例如: # define Pl3.1415926 ,当程序执行 area=Pr * r 语句时,PI会被替换为3.1415926。于是该语句被替换为 area=3.1415926*r*r 。如果把#define语句中的数字9写成了g,预处理也照样代入,而不去检查其是否合理、合法。
typedef 是关键字,它在编译时处理,所以 typedef 具有类型检查的功能。它在自己的作用域内给一个已经存在的类型一个别名,但是不能在一个函数定义里面使用标识符 typedef。例如:typedef int INTEGER ,这以后就可用 INTEGER来代替int作整型变量的类型说明了。
例如0:INTEGER a,b; 用 typedef定义数组、指针、结构等类型将带来很大的方便。
例如1: typedef int a[10]; 表示a是整型数组类型,数组长度为10。然后就可用a说明变量,例如2: 语句a s1,s2;完全等效于语句 int s1[10],s2[10]。
例如3: typedef void(*p)(void)表示p是一种指向void型的指针类型。
二者区别:
类型安全:typedef 是类型安全的,而 #define 不是。
作用域: typedef 具有作用域的概念,而 #define 没有,即 #define 定义的宏在整个文件中都是可见的,除非使用了#undef取消定义。
存储: typedef 定义的类型会占用内存,而 #define 定义的宏不占用内存。
编译阶段:typedef 在编译阶段处理,而 #define 在预处理阶段处理。
使用场景:
typedef 的使用场景:
1.简化复杂的类型定义。
typedef struct {
int id;
char name[50];
float salary;
} Employee;
Employee emp1; // 使用typedef定义的别名
2.为基本数据类型创建别名。
typedef unsigned char byte;
byte data; // 使用typedef定义的别名
3.定义指针类型。
4.定义函数指针类型。
typedef void (*Callback)(int);
Callback cb; // 使用typedef定义的函数指针别名
#define 的使用场景:
1.定义编译时常量:
当需要定义一个在编译时就已经确定的常量值时,可以使用 #define。
2.条件编译:#define 可以用于条件编译,控制代码的编译过程。
#ifdef DEBUG
#include "debug.h"
#endif
3.函数宏:当需要定义一个宏来替代函数调用时,可以使用 #define。
#define SQUARE(x) x*x
int result = SQUARE(2 + 2); // 实际上会展开为 (2 + 2)*2,而不是 2 + (2 + 2)
#define SQUARE(x) ((x)*(x)) // 注意括号的使用
4.避免头文件重复包含:#define 常用于防止头文件被重复包含。
#ifndef HEADER_FILE_H
#define HEADER_FILE_H
// 头文件内容
#endif
2. const 、volatile 和 static 的区别
const
关键字用于定义常量(即定义变量(局部变量或全局变量)为常量)。它表示一个值一旦初始化后就不能被修改。const
可以用于变量、函数参数、函数返回值以及成员函数中。
static是被声明为静态类型的变量,存储在静态区(全局区)中,其生命周期为整个程序,如果是静态局部变量,其作用域为一对{ }内,如果是静态全局变量,其作用域为当前文件。静态变量如果没有被初始化,则自动初始化为0。
volatile的意思是”易变的”,这个关键字主要是防止编译器对变量进行优化。即告诉编译器每次存取该变量的时候都要从内存去存取而不是使用它之前在寄存器中的备份
使用场景:
哪些情况下使用volatile:
1.并行设备的硬件寄存器
存储器映射的硬件寄存器通常加volatile,因为寄存器随时可以被外设硬件修改。
当声明指向设备寄存器的指针时一定要用volatile,它会告诉编译器不要对存储在这个地址的数据进行假设。
2.一个中断服务程序中修改的供其他程序检测的变量。
3.多线程应用中被几个任务共享的变量。
使用 const 的情况:
1.保护数据不被修改:当你想要确保一个变量的值在初始化后不会被改变时,使用 const。
const int MAX_USERS = 100; // 定义一个常量,限制最大用户数
2.定义常量表达式:当一个值在编译时就已知且不应改变时,使用 const。
const int daysInWeek = 7;
3.函数参数和返回值:当你想要确保函数不会修改传入的参数,或者函数返回的值不会被调用者修改时,使用 const。
void process(const DataType& data); // 表示函数不会修改data
const DataType computeValue(); // 表示返回的值不应被修改
4.类的常量成员函数:当你需要一个成员函数不修改类的任何成员变量时,将该成员函数声明为 const。
class MyClass {
public:
int getValue() const; // 常量成员函数
};
使用 static 的情况:
1.全局变量或函数的局部化:当你有一个全局变量或函数,但只想让它在某个特定文件内可见时,使用 static。
static int globalCounter; // 只在当前文件内可见
2.类的静态成员:当你需要一个成员变量或成员函数属于类本身而不是类的某个特定对象时,使用 static。
class MyClass {
public:
static int countObjects; // 静态成员变量
static void staticFunction(); // 静态成员函数
};
3.保持函数内变量的值:当你需要一个函数内的变量在多次函数调用之间保持其值时,使用 static。
void incrementCounter() {
static int counter = 0; // 局部静态变量
counter++;
std::cout << counter << std::endl;
}
// counter变量在函数第一次被调用时初始化,并在后续的函数调用中保持其值。
// 这意味着每次函数调用结束时,局部静态变量的值不会丢失,而是会保留到下一次函数调用。
*4.单例模式:当你实现单例模式,需要确保一个类只有一个实例时,static 是一个关键的关键字。
class Singleton {
private:
static Singleton* instance;
public:
static Singleton* getInstance() {
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
};
3. const修饰指针
(1)指针本身是常量:当 const
放在指针变量的左边时,它修饰的是指针本身,意味着指针的值(即它所指向的地址)不能被改变。但是,它所指向的数据是可以修改的。
const int* ptr;
int a = 10;
ptr = &a; // 正确,ptr指向a
*ptr = 20; // 正确,修改ptr所指向的数据
ptr = &b; // 错误,不能改变ptr的值
(2)指针指向的值是常量:当 const
放在指针变量的右边时,它修饰的是指针所指向的数据,意味着指针所指向的数据不能被改变。但是,指针本身的值(即它所指向的地址)是可以修改的。
int* const ptr = &a; // ptr指向a,并且ptr不能再指向其他地址
*ptr = 20; // 错误,不能修改ptr所指向的数据
ptr = &b; // 正确,可以改变ptr的值,让它指向b
(3)指针本身和指向的值都是常量:如果一个指针变量同时被 const
修饰在左边和右边,那么这个指针既不能指向其他地址,也不能通过这个指针修改它所指向的数据。
const int* const ptr = &a; // ptr既不能改变指向的地址,也不能通过ptr修改数据
*ptr = 20; // 错误,不能修改ptr所指向的数据
ptr = &b; // 错误,不能改变ptr的值
使用场景:
示例1:防止修改传入的数据
在函数中,我们经常需要传递数据,但又不希望函数内部的代码修改这些数据。使用 const 修饰指针参数可以确保这一点。
void printMessage(const char* message) {
// 正确:只能读取message指向的内容,不能修改
std::cout << message << std::endl;
// *message = 'a'; // 错误:尝试修改message指向的内容
}
int main() {
const char greeting[] = "Hello, World!";
printMessage(greeting); // 正确:greeting是const char数组
return 0;
}
示例2:防止修改指针指向的地址
有时我们希望一个函数不改变指针指向的地址,但允许修改指针指向的数据。这可以通过将 const 放在指针的左边来实现。
void increment(int* const ptr) {
// 正确:可以修改ptr指向的数据
*ptr += 1;
// ptr = nullptr; // 错误:不能改变ptr指向的地址
}
int main() {
int value = 10;
increment(&value); // 正确:increment不会改变指针的地址
return 0;
}
示例3:防止修改指针指向的数据和地址
如果既不想修改指针指向的数据,也不想修改指针指向的地址,可以将 const 放在指针的两边。
void observe(const int* const ptr) {
// 正确:只能读取ptr指向的数据,不能修改
std::cout << *ptr << std::endl;
// *ptr = 20; // 错误:不能修改ptr指向的数据
// ptr = nullptr; // 错误:不能改变ptr指向的地址
}
int main() {
const int value = 10;
observe(&value); // 正确:observe不会改变指针指向的数据和地址
return 0;
}
示例4:返回指向常量的指针
当函数返回一个常量值的指针时,使用 const 修饰指针可以防止调用者通过这个指针修改数据。
const int* getConstValue() {
static const int value = 42;
return &value;
}
int main() {
const int* ptr = getConstValue();
// *ptr = 100; // 错误:ptr指向的数据是常量,不能修改
return 0;
}
示例5:保护类的成员函数
在类中,const 修饰的成员函数表示该函数不会修改对象的状态,这样的函数可以被 const 对象调用。
class Counter {
public:
int count() const { // const表示这个成员函数不会修改Counter对象
return value;
}
private:
int value;
};
const Counter c;
std::cout << c.count() << std::endl; // 正确:count()是一个const成员函数
4. 数组指针和指针数组
数组指针是指一个指向数组的指针,它是一个指针,指向一个数组。这意味着它存储的是单个数组的地址,而不是多个指针的集合。
指针数组 是指一个指针数组,它是一个数组,其元素都是指针。换句话说,它是一个存储了多个指针的数组,每个指针可以指向不同类型的数据。
一维数组:
int( * p)[n]; //定义了指向含有n个元素的一维数组的指针
int a[n]; //定义数组
p=a; //将一维数组首地址赋值给数组指针p
二维数组:
int(* p)[4]; //定义了指向含有4个元素的一维数组的指针
int a[3][4];
p=a; //将二维数组的首地址赋值给p,也可是a[0]或&a[0][0]
p++; //表示p跨过行a[0][],指向了行a[1][]
int *p[3]; //定义指针数组
int a[3][4];
for(i=0;i<3;i++)
p[i]=a[i]; //通过循环将a数组每行的首地址分别赋值给p里的元素
示例:
#include <stdio.h>
#include <stdlib.h>
int main() {
int b[12] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
int (*p)[4]; // p是一个数组指针,它指向一个包含有4个int类型数组的指针
p = b;
printf("%d\n", *(*(++p))); // 打印b[4]的值,即5
return 0;
}
5. 函数指针和指针函数
函数指针是一个变量,它存储了函数的地址。这意味着函数指针可以被用来调用函数或者将函数作为参数传递给另一个函数。函数指针的使用提供了一种灵活的方式来处理函数,它们是实现回调函数、事件处理机制和高阶函数的重要工具。
指针函数是一个函数,它的返回值是一个指针。这意味着函数执行完成后,返回的是一个地址,这个地址可以指向一个变量、一个数组、一个结构体或者任何其他数据类型。
int (*funcPtr)(int, int); // 函数指针
int* getIntArray(); // 指针函数
示例:
// 函数指针示例
int add(int a, int b) {
return a + b;
}
int main() {
int (*funcPtr)(int, int) = add; // 函数指针指向add函数
int result = funcPtr(3, 5); // 通过函数指针调用函数
printf("Result: %d\n", result);
return 0;
}
// 指针函数示例
int* createArray(int size) {
int* array = (int*)malloc(size * sizeof(int));
// 初始化array...
return array;
}
int main() {
int* myArray = createArray(10); // 指针函数返回一个指向int数组的指针
// 使用myArray...
free(myArray); // 释放内存
return 0;
}
综合案例:
#include <stdio.h>
// 函数指针声明
float *find(float (*pointer)[4], int n);
int main(void) {
static float score[3][4] = {{60, 70, 80, 90}, {56, 89, 34, 45}, {34, 23, 56, 45}};
float (*p)[4];
int i, m;
printf("Enter the number to be found: ");
scanf("%d", &m);
printf("The score of NO.%d are:\n", m);
p = find(score, m - 1); // 调用find函数,传入score数组和用户输入的编号减1
for (i = 0; i < 4; i++) {
printf("%5.2f\t", (*p)[i]); // 访问并打印指针p指向的数组元素
}
return 0;
}
// 函数指针定义
float *find(float (*pointer)[4], int n) {
return pointer[n]; // 返回第n个数组的地址
}
6. C/C++内存管理
6.1 内存分布图解
内存分布说明:
1.内核空间: 放置操作系统相关的代码和数据。(用户不能直接进行操作 ------ 可以通过调用系统提供的 api 函数)
2.栈:又叫堆栈,非静态局部变量/函数参数/返回值等等,栈是向下增长的。
3.内存映射段:是高效的I/O映射方式,用于装载一个共享的 动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。
4.堆:用于程序运行时动态内存分配,堆是可以上增长的。
5.数据段:存储全局数据和静态数据。
6.代码段:可执行的代码/只读常量。
6.2 C语言中的内存分配方式
1.静态存储区分配:内存分配在程序编译之前完成,且在程序的整个运行期间都存在,例如全局变量、静态变量等。
2.栈上分配:
在函数执行时,函数内的局部变量的存储单元在栈上创建,函数执行结束时这些存储单元自动释放。局部变量、函数内参数都在栈上。
3.堆上分配:New开辟的空间在堆上
6.3 堆(Heap)和栈(Stack)的区别
1.内存管理:
栈内存由操作系统自动管理,通常用于存储函数调用时的上下文信息,如局部变量、参数和返回地址。
堆内存需要程序员手动管理,通过 malloc、calloc、realloc(C语言)或 new、delete(C++语言)进行分配和释放。
2.分配方式:
栈内存分配速度快,因为分配和释放是连续的,且有固定的生命周期。
堆内存分配速度相对较慢,因为需要搜索未分配的内存块,并且可能涉及内存碎片整理。
3.生命周期:
栈内存的生命周期与函数调用相同,函数结束时,栈上的局部变量自动被销毁。
堆内存的生命周期由程序员控制,需要手动释放,否则可能导致内存泄漏。
4.大小限制:
栈内存的大小通常有限,且由操作系统限制,不适合存储大量数据。
堆内存的大小通常比栈内存大,可以存储大量数据,但受限于系统可用内存。
5.存储内容:
栈内存通常用于存储局部变量、函数参数、返回地址和栈帧信息。
堆内存用于存储动态分配的数据结构,如动态数组、对象实例等。
6.内存地址增长:
栈内存的地址是向下增长的,即越接近栈顶的内存地址值越小。
堆内存的地址是向上增长的,即越新分配的内存地址值越大。
7.内存碎片:
栈内存由于其分配和释放的连续性,不会产生内存碎片。
堆内存由于多次分配和释放,可能会产生内存碎片,影响内存使用效率。
8.性能考虑:
栈内存访问速度快,因为其分配和释放速度快,且位于连续的内存区域。
堆内存访问速度相对较慢,因为分配和释放需要额外的时间,且可能涉及内存碎片整理。
9.作用域:
栈内存的作用域限制在函数内部,函数结束后,栈内存中的数据不再可访问。
堆内存的作用域可以是全局的,只要程序没有释放该内存,数据就可以被访问。
6.4 栈在C语言中的作用
(a)C语言中栈用来存储临时变量,临时变量包括函数参数和函数内部定义的临时变量。函数调用中和函数调用相关的函数返回地址,函数中的临时变量,寄存器等均保存在栈中,函数调动返回后从栈中恢复寄存器和临时变量等函数运行场景。
(b)多线程编程的基础是栈,栈是多线程编程的基石,每一个线程都最少有一个自己专属的栈,用来存储本线程运行时各个函数的临时变量和维系函数调用和函数返回时的函数调用关系和函数运行场景。 操作系统最基本的功能是支持多线程编程,支持中断和异常处理,每个线程都有专属的栈,中断和异常处理也具有专属的栈,栈是操作系统多线程管理的基石。
6.5 C++的内存管理
在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。
1.代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
2.数据段:存储程序中已初始化的全局变量和静态变量
3.BSS段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。
4.堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。
5.映射区:存储动态链接库以及调用mmap函数进行的文件映射
6.栈:使用栈空间存储函数的返回地址、参数、局部变量、返回值
6.6 内存泄漏
内存泄漏(Memory Leak)是指程序在动态分配内存后,由于某种原因未能释放或无法释放这些内存,导致系统内存的浪费。这种现象在程序运行时可能不会立即显现出危害,但随着泄漏的内存逐渐累积,最终可能导致系统性能下降、程序崩溃或系统变慢等问题
内存泄漏的常见原因包括:
1.未释放动态分配的内存:在C/C++等需要手动管理内存的编程语言中,如果程序员忘记或错误地释放已经分配的内存,就会导致内存泄漏。
2.引用计数错误:在一些语言中,内存管理是通过引用计数实现的。如果程序中存在引用计数错误,例如增加引用计数但未相应减少,就会导致内存泄漏。
3.循环引用:当两个或多个对象相互引用,形成一个循环链表,如果这些对象没有被其他部分访问,但它们之间存在引用,那么它们的引用计数永远不会为零,导致内存泄漏。
4.文件未关闭:在一些应用中,程序可能会打开文件或网络连接,但在使用完毕后未正确关闭,这样会导致系统资源泄漏,包括内存泄漏。
5.内存碎片:内存泄漏不仅仅是指没有释放的内存,还包括内存碎片。当程序频繁分配和释放小块内存时,可能会在内存中留下碎片,最终导致内存不足。
解决内存泄漏的方法包括:
1.使用内存检测工具:例如Valgrind、AddressSanitizer等,这些工具可以在运行时检测到未释放的内存,并提供详细的报告。
2.自动内存管理:在支持自动垃圾回收的语言中,如Java,可以利用垃圾回收机制来减少内存泄漏的发生。
3.良好的编程习惯:及时释放不再使用的内存,避免不必要的内存分配,以及使用智能指针等现代C++特性来管理内存。
4.内存池技术:通过内存池来管理内存分配和释放,可以减少内存碎片,提高内存使用效率。
5.定期审查代码:通过代码审查来发现和修复可能导致内存泄漏的代码段。
6.7 new/delete与malloc/free的区别
在C/C++中,申请动态内存和释放动态内存,用new/delete 和 malloc/free都可以,new和malloc动态申请的内存都位于堆中,无法被操作系统回收,需要对应的delete/free来释放空间。
函数malloc 的原型如下:
void * malloc(size_t size);
用malloc 申请一块长度为length 的整数类型的内存,程序如下:
int *p = (int *) malloc(sizeof(int) * length);
PS:
malloc 返回值的类型是void *,所以在调用malloc 时要显式地进行类型转换,将void * 转换成所需要的指针类型。
malloc 函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。
new/delete 的使用要点
例如:
int *p1 = (int *)malloc(sizeof(int) * length);
int *p2 = new int[length];
delete pi;
这是因为new 内置了sizeof、类型转换和类型安全检查功能。
对于非内部数据类型的对象而言,new 在创建动态对象的同时完成了初始化工作。
如果对象有多个构造函数,那么new 的语句也可以有多种形式。
C++中允许动态创建const对象:
const int *pi=new const int(1024);
动态创建的const对象必须进行初始化,并且进行初始化后的值不能再改变。
当创建一个动态数组对象和进行内存释放时,执行以下语句:
int *pi=new int[]; // 指针pi所指向的数组未初始化
int *pi=new int[n]; // 指针pi指向长度为n的数组,未初始化
int *pi=new int[](); // 指针pi所指向的地址初始化为0
delete [] pi; // 回收pi所指向的数组
区别:对于类的对象而言,malloc/free无法满足动态对象的要求,对象在创建时要自动执行构造函数,在对象消亡之前要自动执行析构函数,而malloc/free 不在编译器控制权限之内,无法执行构造函数和析构函数。
a.属性
new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持c。
b.参数
使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。
c.返回类型
new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。
而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。
delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。
malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
使用场景:在这个例子中,当使用 new
分配 MyClass
对象时,构造函数被自动调用,初始化对象的 value
成员。当对象不再需要时,使用 delete
释放内存,同时析构函数被调用,清理对象。
#include <iostream>
class MyClass {
public:
int value;
MyClass(int val) : value(val) { // 构造函数
std::cout << "Constructor called with value " << value << std::endl;
}
~MyClass() { // 析构函数
std::cout << "Destructor called for value " << value << std::endl;
}
};
int main() {
MyClass* obj = new MyClass(10); // 使用new分配内存并调用构造函数
// 使用obj...
delete obj; // 使用delete释放内存并调用析构函数
return 0;
}
7. C语言部分
7.1 # include< filename. h>和#include" filename. h"的区别
对于 include< filename. h>,编译器先从标准库路径开始搜索filename.h,使得系统文件调用较快。而对于# include“ filename.h”,编译器先从用户的工作路径开始搜索filename.h,然后去寻找系统路径,使得自定义文件较快。
7.2 在头文件中定义静态变量?
如果在头文件中定义静态变量,会造成资源浪费的问题,同时也可能引起程序错误。因为如果 在使用了该头文件的每个C语言文件中定义静态变量,按照编译的步骤,在每个头文件中都会单独存在一个静态变量,从而会引起空间浪费或者程序错误所以,不推荐在头文件中定义任何变量,当然也包括静态变量。
7.3 C语言宏中“#”和“##”的用法
7.3.1 字符串化:使用“#”运算符可以将宏参数转换为字符串,这在需要将代码中的值或表达式转换为字符串时非常有用。例如,用于日志记录或调试输出:
#define LOG_ERROR(msg) printf("Error: " #msg "\n")
LOG_ERROR(Invalid user input);
7.3.2 Token 连接: 使用“##”运算符可以将两个token连接在一起,这在创建基于宏参数的复合标识符时非常有用。例如,用于生成函数名或变量名:
#define CREATE_VAR(prefix, suffix) prefix ## suffix
int CREATE_VAR(my, counter) = 0; // 创建变量 mycounter
7.3.3 条件编译:结合“#”和“##”运算符,可以实现复杂的条件编译逻辑,例如根据编译时的条件生成不同的代码片段:
#define DEBUG 1
#if DEBUG
#define DEBUG_MSG(msg) printf("Debug: " #msg "\n")
#else
#define DEBUG_MSG(msg)
#endif
DEBUG_MSG(This is a debug message);
如果DEBUG
被定义为1,DEBUG_MSG
将输出调试信息;否则,DEBUG_MSG
将是一个空操作。
7.4 struct与 union的区别
1.内存分配:
结构体为每个成员分配内存空间。即使成员是同一类型的,它们也会各自占用自己的内存空间。因此,结构体的大小是所有成员大小的总和(内存对齐)。
联合体的所有成员共享同一块内存空间。在任何时刻,联合体只能存储其中一个成员的值。联合体的大小等于其最大成员的大小。
2.成员赋值:
对于联合体的不同成员赋值,将会对它的其他成员重写,原来成员的值就不存在了。
对结构体的不同成员赋值是互不影响的。
3.初始化:
结构体可以被初始化为包含所有成员的初始值。
联合体只能初始化为其中一个成员的值。
7.5 左值和右值
左值 是指可以出现在等号左边的变量或表达式,它最重要的特点就是可写(可寻址)。也就是说,它的值可以被修改,如果一个变量或表达式的值不能被修改,那么它就不能作为左值。
右值 是指只可以出现在等号右边的变量或表达式。它最重要的特点是可读。一般的使用场景都是把一个右值赋值给一个左值。通常,左值可以作为右值,但是右值不一定是左值。
7.6 有符号数和无符号数的运算
int a = -20, unsigned int b = 6,a+b是否大于6?
有符号和无符号运算,强制转换为无符号,所有a+b会变成(unsigned int)a+b;
(unsigned int)a 就会相当于无符号最大值-20,那么是一个非常大的值,这个值加上6,那么肯定是大于6的;
结果:2^32-20+6=4294967282
7.7 短路求值
短路求值(short-circuit evaluation)是一种逻辑运算符的求值策略,主要应用于逻辑与(&&)和逻辑或(||)操作符。这种策略的核心在于,当逻辑表达式的结果可以通过部分操作数的值确定时,就不再对剩余的操作数进行求值。
示例:
bool b1 = false;
bool b2 = true;
bool b3 = b1 && b2; // b3的值为false,因为b1为false,所以不会对b2求值
bool b4 = b1 || b2; // b4的值为true,因为b1为false,所以会对b2求值
7.8 大端和小端
大端:高地址存低字节,低地址存高字节
小端:低地址存低字节,高地址存高字节