C-语言特性相关
左值和右值
左值和右值是C/C++编程语言中的两个重要概念,它们在赋值、引用以及类型转换等方面表现出明显的区别。
定义:
- 左值:表示存储在计算机内存中的对象,具有持久性和可寻址性。即,左值能够用“取地址&”运算符获得其内存地址,且在表达式结束后依然存在。左值可以出现在赋值语句的左侧,也可以出现在右侧。
- 右值:与左值相对,右值通常表示临时对象,它们不具有持久性和可寻址性。即,右值不能用“取地址&”运算符获得其内存地址,且在表达式结束后就不再存在。右值只能出现在赋值语句的右侧。
左/右值引用
- 左值引用:左值引用是对左值的引用,即给左值取别名。左值引用在C++中的内部实现通常是一个常量指针,左值引用可以引用非常量左值和常量左值,但不能直接引用右值。
- 右值引用:右值引用是C++11中引入的新特性,用于引用右值。它允许程序员延长临时对象的生命周期,并在需要时移动资源而不是复制资源。用
&&
表示,右值引用只能绑定到右值上,不能绑定到左值上(除非通过std::move
强制转换)
转换
- 左值到右值的隐式转换:在大多数情况下,左值可以隐式地转化为右值。例如,在赋值操作中,左侧的左值会“退化”为右值,以便与右侧的右值进行匹配。
- 右值到左值的显式转换:右值到左值的显式转化通常是不允许的,因为右值在表达式结束后就不再存在。然而,通过
std::move
可以将左值强制转化为右值引用,从而允许对其进行移动操作。
std::move
-
类型转换: 并不实际移动任何数据,而是将其参数转换为右值引用,从而允许利用移动语义(如果可用)进行资源的转移,而不是复制。
-
函数原型:
std::move()
函数原型:move
函数是将任意类型的左值转为其类型的右值引用。
指针
在C++中,指针是一种非常基础且强大的特性,它允许你直接访问和操作内存地址。指针存储了变量的内存地址,而不是变量的值本身。
- 大小:
sizeof
获得
std::cout << "Size of pointer: " << sizeof(int*) << std::endl; // 输出指针大小
- 用法:基本用法包括声明、初始化、解引用(或取值)、指针运算等
//声明和初始化
int a = 10;
int* ptr = &a; // ptr 是一个指向 int 类型的指针,它存储了变量 a 的地址
//解引用,使用 * 运算符来访问指针所指向的值:
std::cout << *ptr << std::endl; // 输出 10
//指针运算,指针可以进行算术运算,但仅限于加上或减去整数(表示偏移量),以及指针之间的比较。
int arr[5] = {1, 2, 3, 4, 5};
int* p = arr; // p 指向数组的第一个元素
std::cout << *(p + 2) << std::endl; // 输出 3,因为 p+2 指向了数组的第三个元素
- 注意事项:
- 空指针:未初始化的指针是未定义的,可能指向任意内存地址。通常将指针初始化为
nullptr
(C++11及以后)或NULL
(C++11之前,但在C++11中已不推荐使用)来避免野指针问题。 - 越界访问:指针运算时要小心,确保不要越界访问内存。
- 悬挂指针:如果指针指向的对象被删除或释放,但该指针未被设置为
nullptr
,则该指针成为悬挂指针。尝试通过悬挂指针访问内存是未定义行为。 - 内存泄漏:使用动态内存分配(如
new
)时,要确保在适当的时候释放内存(使用delete
),以避免内存泄漏。 - 智能指针:C++11及以后版本中引入了智能指针(如
std::unique_ptr
、std::shared_ptr
等),它们可以自动管理动态分配的内存,减少内存泄漏的风险。 - 指针和数组:在C++中,数组名在大多数情况下会被视为指向数组首元素的指针。但是,这种用法有一些陷阱,比如数组名本身不是一个可修改的左值。
指针和引用区别
指针和引用的主要区别可以简单归纳如下:
-
定义与性质:
- 指针:是一个变量,存储的是另一个变量的内存地址。
- 引用:是某个变量的别名,与原变量共享同一块内存空间,实质上是同一个东西。
-
内存分配:
- 指针:需要分配内存空间来存储地址值。
- 引用:不需要分配额外的内存空间,它只是一个别名。
-
初始化和赋值:
- 指针:可以在定义时不初始化,之后可以指向任何有效的内存地址,包括NULL。
- 引用:必须在定义时初始化,且之后不能再改变为引用另一个变量。
-
多级性:
- 指针:可以有多级,例如指向指针的指针。
- 引用:只能是一级的,不能有多级引用。
-
空值:
- 指针:可以为空(NULL或nullptr)。
- 引用:不能为NULL,必须始终引用一个有效的对象。
-
sizeof运算符:
- 指针:使用sizeof返回的是指针本身的大小(通常是4字节或8字节,取决于系统架构)。
- 引用:使用sizeof返回的是被引用变量的大小。
-
运算操作:
- 指针:支持算术运算(如加减)和比较运算。
- 引用:不支持算术运算,主要用于别名访问。
-
函数参数:
- 指针和引用作为函数参数时,都可以改变实参的值,但引用在语法上更为简洁,且使用上更安全。
-
安全性:
- 指针:使用时需要更加小心,因为错误的指针操作可能导致程序崩溃或安全问题。
- 引用:相对更安全,因为它在定义时必须初始化,并且不能改变为引用另一个变量。
常量指针和指针常量
常量指针和指针常量在C++中是两种常见的指针类型,尽管它们都涉及到const
关键字,但它们的含义和应用场景存在明显的区别。以下是对这两种指针的详细比较:
本质区别
- 常量指针:本质上是一个指针,
const
修饰的是指针指向的内容,表示该指针指向一个“常量”,即指针指向的变量的值不能通过该指针来改变。 - 指针常量:本质上是一个常量,
const
修饰的是指针本身,表示该常量是一个指针类型的常量,即指针的值(即它所指向的地址)不能改变。
声明方式
- 常量指针:通常声明为
const 数据类型 *指针变量名
,例如const int *p = &a;
,这里p
是一个指向整型常量的指针,不能通过p
来修改a
的值,但p
可以指向另一个整型变量的地址。 - 指针常量:通常声明为
数据类型 * const 指针变量名
,例如int *const q = &a;
,这里q
是一个指针常量,它的值(即它指向的地址)不能被修改,但可以通过q
来修改它所指向的变量的值(如果那个变量不是常量的话)。
应用场景
-
常量指针:常用于需要保护数据不被意外修改的场景,同时又想通过指针来访问这些数据。
-
指针常量:常用于需要确保指针始终指向同一个地址的场景,比如某些资源的句柄或引用,一旦初始化后就不应该再指向其他地址。
-
常量指针示例:
int a = 10; const int *p = &a; // p 是常量指针,指向 a // *p = 20; // 编译错误,不能通过 p 修改 a 的值 p = &b; // 正确,p 可以指向另一个变量 b
-
指针常量示例:
int a = 10; int *const q = &a; // q 是指针常量,指向 a *q = 20; // 正确,可以通过 q 修改 a 的值 // q = &b; // 编译错误,q 不能指向其他地址
函数指针
函数指针是C和C++语言中的一个特性,它允许程序员将函数的地址存储在变量中,并通过这个变量来调用函数。函数指针的概念是高级且强大的,因为它提供了一种机制来动态地调用不同的函数,基于运行时条件。
函数指针的声明
函数指针的声明需要指定函数返回值的类型、函数名(在这里我们使用指针名代替)以及函数的参数列表(包括参数的类型和数量)。但是,在声明函数指针时,我们不需要函数名,而是使用指针名来引用这个函数。
例如,假设我们有一个函数原型如下:
int add(int a, int b);
要声明一个指向这个函数的指针,我们可以这样写:
int (*ptrToAdd)(int, int);
这里,ptrToAdd
是一个指针,它指向一个函数,该函数接受两个int
类型的参数并返回一个int
类型的值。
函数指针的赋值
一旦我们声明了函数指针,就可以将函数的地址赋给它。在C和C++中,函数名就代表了函数的地址,因此我们可以直接将函数名赋给函数指针。
ptrToAdd = add; // 将add函数的地址赋给ptrToAdd
通过函数指针调用函数
有了函数指针之后,我们就可以通过这个函数指针来调用函数了。调用方式是通过解引用函数指针,并像调用普通函数一样传递参数。
int result = ptrToAdd(5, 3); // 等同于调用 add(5, 3)
函数指针的应用
函数指针在C和C++中有很多应用,包括但不限于:
- 回调函数:在某些API中,你可以将函数指针作为参数传递给另一个函数,这个被传递的函数指针指向的函数会在某个特定事件发生时被调用。
- 排序函数:例如,在C标准库中的
qsort
函数,它接受一个比较函数的指针作为参数,这个比较函数用于定义排序的准则。 - 动态函数表:通过函数指针数组,可以实现基于运行时条件的函数选择,这在实现多态或构建插件系统时非常有用。
- 事件处理:在图形用户界面(GUI)编程中,事件处理函数通常是通过函数指针来指定的,这样当特定事件(如按钮点击)发生时,相应的函数就会被调用。
值/引用/指针传递
在参数传递中,值传递、引用传递和指针传递是C++(以及C语言)中常见的三种方式,它们各自具有不同的特点和用途。以下是这三种传递方式的详细区别:
值传递(Pass by Value)
定义:值传递时,形参是实参的副本(复制、拷贝)。即函数接收的是实参的一个拷贝,函数体内对形参的任何修改都不会影响到实参。
特点:
- 单向性:数据只能从实参传递到形参,不能反向传递。
- 独立性:形参和实参是两个独立的变量,它们占据不同的内存空间。
- 效率:对于大型对象或结构体,值传递可能会导致较高的内存开销和性能损耗,因为需要复制整个对象。
示例:
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
// 这里a和b的交换不会影响实参
}
引用传递(Pass by Reference)
定义:引用传递时,实参的引用(即内存地址)被传递给形参。形参和实参指向同一块内存地址,因此函数体内对形参的修改会直接影响到实参。
特点:
- 双向性:数据可以在实参和形参之间双向传递。
- 共享性:形参和实参共享同一块内存空间,对形参的修改会反映到实参上。
- 效率:对于大型对象或结构体,引用传递可以避免不必要的复制,提高效率。
示例:
void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
// 这里a和b的交换会影响实参
}
指针传递(Pass by Pointer)
定义:指针传递时,实参的地址(即指针)被传递给形参。形参是一个指针变量,它存储了实参的地址,因此函数体内可以通过解引用形参来修改实参的值。
特点:
- 间接访问:通过指针可以间接访问和修改实参的值。
- 灵活性:指针传递提供了更高的灵活性,可以动态地修改多个变量的值。
- 风险:指针操作相对复杂,容易出错,如野指针、空指针解引用等问题。
示例:
void swap(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
// 这里通过解引用指针来交换实参的值
}
适用
- 值传递:适用于不需要修改实参的场景,或者实参是基本数据类型时。
- 引用传递:适用于需要修改实参的场景,特别是当实参是大型对象或结构体时,可以提高效率。
- 指针传递:提供了更高的灵活性,但也需要更谨慎的操作,以避免指针相关的错误。
迭代器
迭代器模式(Iterator Pattern)是一种行为型设计模式,它提供了一种方法顺序访问一个容器对象中的各个元素,而又不暴露其内部的细节。
迭代器模式的核心思想是将集合的遍历功能从集合对象中分离出来,形成一个独立的迭代器对象来管理访问集合元素的逻辑。
-
迭代器为不同数据结构提供统一的访问接口,使得遍历数据变得简单统一,同时支持高效处理大型数据集,通过逐个访问元素节省内存资源。
-
迭代器不仅限于遍历,还能与算法结合实现复杂数据处理,支持惰性计算,提升代码可读性和可维护性,是现代编程中不可或缺的工具。
野指针和悬空指针
野指针和悬空指针是编程中常见的两种指针问题,它们通常会导致程序出现不可预测的行为和潜在的错误。
野指针(Wild Pointer)
定义:
野指针是指向一个已删除的对象或未申请访问受限内存区域的指针。野指针的值是不确定的,可能指向任何位置,包括操作系统不允许访问的内存区域。
危害:
访问野指针通常会导致程序崩溃或未定义行为,因为它可能指向任意内存地址,包括操作系统不允许访问的内存区域。
成因:
- 指针变量未初始化:任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的。如果指针变量在创建时没有初始化,它将指向一个不确定的内存地址,从而成为野指针。
- 指针释放之后未置空:有时指针在free或delete后未赋值NULL,此时指针仍然指向已被释放的内存地址,但该内存可能已经被系统重新分配给其他对象,因此该指针成为野指针。
- 指针操作超越变量作用域:例如,函数返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放,返回的指针将成为野指针。
预防措施:
- 初始化时置NULL:指针变量在创建时应立即初始化为NULL或有效的内存地址。
- 释放时置NULL:当指针指向的内存空间释放时,应立即将指针置为NULL,防止产生野指针。
- 避免返回局部变量的地址:不要从函数中返回局部变量的地址,因为局部变量在函数返回后会被销毁。
悬空指针(Dangling Pointer)
定义:
悬空指针是指一个曾经指向有效内存区域,但由于该内存区域已经被释放或变得无效,因此现在指向一个不再可用的内存地址的指针。
危害:
尝试通过悬空指针访问内存通常会导致程序崩溃或未定义行为,因为指针指向的内存区域已经不再可用。
成因:
- 内存释放后继续使用:动态分配的内存被释放(如使用free()函数),但指针仍然指向原来的内存地址。
- 函数返回局部变量的地址:与野指针类似,但悬空指针特指在内存被释放后仍然指向该内存地址的情况。
- 使用已经释放的资源:例如,文件被关闭后,仍然使用指向该文件的指针。
预防措施:
- 释放内存后置空指针:在使用free()或delete释放动态分配的内存后,立即将指针置为NULL。
- 避免返回局部变量的地址:同上,不要从函数中返回局部变量的地址。
- 谨慎使用动态内存分配:尽量减少动态内存分配的使用,特别是在不需要的情况下。如果必须使用,确保正确管理内存的生命周期。
nullptr 与 NULL
- NULL:预处理变量,是一个宏,它的值是 0,定义在头文件 中,即 #define NULL 0。
- nullptr:C++ 11 中的关键字,是一种特殊类型的字面值,可以被转换成任意其他类型。
nullptr优势:
- 有类型,类型是 typdef decltype(nullptr) nullptr_t;,使用 nullptr 提高代码的健壮性。
- 函数重载:因为 NULL 本质上是 0,在函数调用过程中,若出现函数重载并且传递的实参是 NULL,可能会出现不知和哪一个函数匹配的情况;但是传递实参 nullptr 就不会出现这种情况。
原文地址:https://blog.csdn.net/longer_net/article/details/140175413
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!