C/C++教程

c++基础知识点(三)

本文主要是介绍c++基础知识点(三),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

21、const关键字:修饰变量、指针、类对象、类中成员函数

1、什么是const
const是一个C++语言的限定符,它限定一个变量不允许被改变。使用const在一定程度上可以提高程序的安全性和可靠性。
只要一个变量前用const来修饰,就意味着该变量里的数据只能被访问,而不能被修改,也就是意味着const“只读”(readonly)
const 是 C++ 中的关键字,它会在编译期间(时机很重要),告诉编译器这个对象是不能被修改的。
规则:

const离谁近,谁就不能被修改;
const修饰一个变量时,一定要给这个变量初始化,若不初始化,在后面也不能初始化。
const作用:

1:可以用来定义常量,修饰函数参数,修饰函数返回值,且被const修饰的东西,都受到强制保护,可以预防其它代码无意识的进行修改,从而提高了程序的健壮性(是指系统对于规范要求以外的输入能够判断这个输入不符合规范要求,并能有合理的处理方式。ps:即所谓高手写的程序不容易死);
2:使编译器保护那些不希望被修改的参数,防止无意代码的修改,减少bug;
3:给读代码的人传递有用的信息,声明一个参数,是为了告诉用户这个参数的应用目的;

const优点:

1:编译器可以对const进行类型安全检查(所谓的类型安全检查,能将程序集间彼此隔离开来,这种隔离能确保程序集彼此间不会产生负面影响,提高程序的可读性);

2:有些集成化的调试工具可以对const常量进行调试,使编译器对处理内容有了更多的了解,消除了一些隐患。

eg:void hanshu(const int i){.......} 编译器就会知道i是一个不允许被修改的常量
3:可以节省空间,避免不必要的内存分配,因为编译器通常不为const常量分配内存空间,而是将它保存在符号表中,这样就没有了存储于读内存的操作,使效率也得以提高;

4:可以很方便的进行参数的修改和调整,同时避免意义模糊的数字出现;

2.1、const全局/局部变量

const在修饰全局变量时第一个作用,会限定全局变量的作用范围到其定义时所在的编译单元。

const全局变量使得我们指定了一个语义约束,即被修饰的全局变量不允许被修改,而编译器会强制实施这个约束。

const局部变量被修改了,但是在使用变量名输出时,编译器会出现一种类似宏定义的功能一样的行为,将变量名替换为初始值。可见,const局部变量并不能做到真正的不变,而是编译器对其进行了一些优化行为,这导致了const局部变量与真实值产生了不一致。(常量折叠现象)

那么,如果想获取修改后的const局部变量真实值,该怎么办呢?答案是使用volatile关键字。volatile关键字使得程序每次直接去内存中读取变量值而不是读寄存器值,这个作用在解决一些不是程序而是由于别的原因修改了变量值时非常有用。

2.2、cosnt修饰指针和引用
cosnt修饰指针
const修饰指针,涉及到两个很重要的概念,顶层const和底层cosnt

指针自身是一个对象,它的值为一个整数,表明指向对象的内存地址。因此指针长度所指向对象类型无关,在32位系统下为4字节,64位系统下为8字节。进而,指针本身是否是常量以及所指向的对象是否是常量就是两个独立的问题。

从 const 指针开始说起。const int* pInt; 和 int *const pInt = &someInt;,前者是 *pInt 不能改变,而后者是 pInt 不能改变。因此指针本身是不是常量和指针所指向的对象是不是常量就是两个互相独立的问题。用顶层表示指针本身是个常量,底层表示指针所指向的对象是个常量。

更一般的,顶层 const 可以表示任意的对象是常量,这一点对任何数据类型都适用;底层 const 则与指针和引用等复合类型有关,比较特殊的是,指针类型既可以是顶层 const 也可以是底层 const 或者二者兼备。

cosnt修饰引用

常引用所引用的对象不能更新,使用方法为:const 类型说明符 &引用名。

非const引用只能绑定非const对象,const引用可以绑定任意对象,并且都当做常对象。

常引用经常用作形参,防止函数内对象被意外修改。对于在函数中不会修改其值的参数,最好都声明为常引用。复制构造函数的参数一般均为常引用。

2.3、const修饰函数参数
const修饰参数是为了防止函数体内可能会修改参数原始对象。因此,有三种情况可讨论:

1、函数参数为值传递:值传递(pass-by-value)是传递一份参数的拷贝给函数,因此不论函数体代码如何运行,也只会修改拷贝而无法修改原始对象,这种情况不需要将参数声明为const。
2、函数参数为指针:指针传递(pass-by-pointer)只会进行浅拷贝,拷贝一份指针给函数,而不会拷贝一份原始对象。因此,给指针参数加上顶层const可以防止指针指向被篡改,加上底层const可以防止指向对象被篡改。
3、函数参数为引用:引用传递(pass-by-reference)有一个很重要的作用,由于引用就是对象的一个别名,因此不需要拷贝对象,减小了开销。这同时也导致可以通过修改引用直接修改原始对象(毕竟引用和原始对象其实是同一个东西),因此,大多数时候,推荐函数参数设置为pass-by-reference-to-const。给引用加上底层const,既可以减小拷贝开销,又可以防止修改底层所引用的对象。

2.4、const修饰函数返回值

const修饰函数返回值的含义和用const修饰普通变量以及指针的含义基本相同。这样可以防止外部对 object 的内部成员进行修改。

2.5、const成员函数和数据成员
类的常成员函数

由于C++会保护const对象不被更新,为了防止类的对象出现意外更新,禁止const对象调用类的非常成员函数。因此,常成员函数为常对象的唯一对外接口。

常成员函数的声明方式:类型说明符 函数名(参数表) const

const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数;
const对象的成员是不能修改的,而通过指针维护的对象确实可以修改的;
const成员函数不可以修改对象的数据,不管对象是否具有const性质。编译时以是否修改成员数据为依据进行检查。

2.6、const修饰类对象

用const修饰的类对象,该对象内的任何成员变量都不能被修改。
因此不能调用该对象的任何非const成员函数,因为对非const成员函数的调用会有修改成员变量的企图。

4、const与宏定义的区别
(1) 编译器处理方式不同
  define宏是在预处理阶段展开。
  const常量是编译运行阶段使用。

(2) 类型和安全检查不同
  define宏没有类型,不做任何类型检查,仅仅是展开。
  const常量有具体的类型,在编译阶段会执行类型检查。

(3) 存储方式不同
  define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。
  const常量会在内存中分配(可以是堆中也可以是栈中)。

(4)const 可以节省空间,避免不必要的内存分配。

22、extern关键字:修饰全局变量

extern关键字主要是声明这个变量已经在其他文件中声明过了(声明全局变量或者函数),如果编译过程中遇到带有extern的变量,就会去其他模块中寻找它的定义。但是注意,它只是声明而不是定义,也就是说,如果想要使用这个变量,只需要包含这个变量定义所在的位置的头文件即可。在编译阶段,虽然本模块找不到该函数或者变量,但是不会报错,会在连接时从定义的模块中找到该变量或者函数。
总之,extern就是用来声明这个东西已经在其他文件中声明过了。不管是变量还是函数,都可以使用extern函数这样声明。不过函数不使用extern和使用extern关键字声明没有太大的区别。而对于变量,如果全局变量定义和使用不在一个文件中,就会发生未定义的错误。而如果在这个文件中重新定义一个同名的变量,又会发生重定义的错误。所以,这种情况就需要使用extern关键字在本文件中声明即可。

关于extern和static:
两者其实是水火不容,因为extern的作用为让变量或者函数跨文件,而static的作用是让变量或者函数的作用域保留在本文件中,所以二者不能同时使用。
一般定义static全局变量都是在实现文件中,而不会再跑到头文件中去声明一份。

23、volatile关键字:避免编译器指令优化

volatile关键字是一种类型修饰符,用它声明的类型变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。 精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。

 

其中关键在于两个地方:     

1. 编译器的优化

在本次线程内, 当读取一个变量时,为提高存取速度,编译器优化时有时会先把变量读取到一个寄存器中;以后,再取变量值时,就直接从寄存器中取值;

当变量值在本线程里改变时,会同时把变量的新值copy到该寄存器中,以便保持一致

当变量在因别的线程等而改变了值,该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致

当该寄存器在因别的线程等而改变了值,原变量的值不会改变,从而造成应用程序读取的值和实际的变量值不一致 

2. 在什么情况下会用到

 

  • 并行设备的硬件寄存器(如:状态寄存器) 
  • 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables) 
  • 多线程应用中被几个任务共享的变量  

 

再附一篇写得不错的介绍文章:

用volatile关键字防止变量被编译器优化

volatile 是在C、C++、Java等中语言中的一种修饰关键字。

这个关键字在嵌入式系统中,是一个非常重要的一个使用。尽管在一般的Application中,可能很多人都不需要使用这个。但是在单片机中,如果不熟悉这个关键字,很有可能产生想像不到的意外。

那么,我就来谈谈Volatile的意义–

volatile在ANSI C(C89)以后的C标准规格和const一起被包含在内。在标准C中,这些关键字是必定存在的。

关于volatile的意义,根据标准C的定义、volatile的目的是,避免进行默认的优化处理.比如说对于编译器优化的功能,如果从编译器看来,有些多余的代码的话,编译器就会启动优化程序,并删除一些代码,但是这在嵌入式系统中很有可能是关键性的处理,必须不能保证被编译器删掉,所以提供了Volitile来声明,告诉编译器无论如何都不要删掉我。

 

 

24、四种类型转换:static_cast、dynamic_cast、const_cast、reinterpret_cast

  • static_cast

相当于c语言强制转换

char a = 'a';
int b = static_cast<char>(a);//正确,将char型数据转换成int型数据

double *c = new double;
void *d = static_cast<void*>(c);//正确,将double指针转换成void指针

int e = 10;
const int f = static_cast<const int>(e);//正确,将int型数据转换成const int型数据

const int g = 20;


int *h = static_cast<int*>(&g);//编译错误,static_cast不能转换掉g的const属性

  • dynamic_cast

  • type必须是一个类类型,在第一种形式中,type必须是一个有效的指针,在第二种形式中,type必须是一个左值,在第三种形式中,type必须是一个右值。在上面所有形式中,e的类型必须符合以下三个条件中的任何一个:e的类型是是目标类型type的公有派生类、e的类型是目标type的共有基类或者e的类型就是目标type的的类型。如果一条dynamic_cast语句的转换目标是指针类型并且失败了,则结果为0。如果转换目标是引用类型并且失败了,则dynamic_cast运算符将抛出一个std::bad_cast异常(该异常定义在typeinfo标准库头文件中)。e也可以是一个空指针,结果是所需类型的空指针。
  • const_cast

const_cast,用于修改类型的const或volatile属性。 

该运算符用来修改类型的const(唯一有此能力的C++-style转型操作符)或volatile属性。除了const 或volatile修饰之外, new_type和expression的类型是一样的。 ①常量指针被转化成非常量的指针,并且仍然指向原来的对象; ②常量引用被转换成非常量的引用,并且仍然指向原来的对象; ③const_cast一般用于修改底指针。如const char *p形式。
  • reinterpret_cast

new_type必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,再把该整数转换成原类型的指针,还可以得到原先的指针值)。

reinterpret_cast意图执行低级转型,实际动作(及结果)可能取决于编辑器,这也就表示它不可移植

25、右值引用

lvalue 是“loactor value”的缩写,可意为存储在内存中、有明确存储地址(可寻址)的数据,而 rvalue 译为 "read value",指的是那些可以提供数据值的数据(不一定可以寻址,例如存储于寄存器中的数据)。

C++11 标准新引入了另一种引用方式,称为右值引用,用 "&&" 表示。右值往往是没有名称的,因此要使用它只能借助引用的方式。这就产生一个问题,实际开发中我们可能需要对右值进行修改(实现移动语义时就需要),显然左值引用的方式是行不通的。

int && a = 10;
a = 100;
cout << a << endl;

26、std::move函数

在C++11中,标准库在<utility>中提供了一个有用的函数std::move,std::move并不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义。从实现上讲,std::move基本等同于一个类型转换:static_cast<T&&>(lvalue);

std::move函数可以以非常简单的方式将左值引用转换为右值引用。(左值 右值 引用 左值引用)概念 https://blog.csdn.net/p942005405/article/details/84644101

C++ 标准库使用比如vector::push_back 等这类函数时,会对参数的对象进行复制,连数据也会复制.这就会造成对象内存的额外创建, 本来原意是想把参数push_back进去就行了,通过std::move,可以避免不必要的拷贝操作。
std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝所以可以提高利用效率,改善性能.。
对指针类型的标准库对象并不需要这么做.

27、四种智能指针及底层实现:auto_ptr、unique_ptr、shared_ptr、weak_ptr

1. auto_ptr
auto_ptr主要是用来解决资源自动释放的问题,在函数遇到错误之后,一般会抛异常,或者返回,但是这时很可能遗漏之前申请的资源,及时是很有经验的程序员也有可能出现这种错误,

 

 

 

而使用auto_ptr会在自己的析够函数中进行资源释放。也就是所说的RAII

2. unique_ptr
unique_ptr可以看成是auto_ptr的替代品。因为他对对象的所有权比较专一,所以才叫unique 大笑
i) 无法进行复制构造和赋值操作
auto_ptr与unique_ptr的对比:
auto_ptr<Obj> ap(new Obj() );
auto_ptr<Obj> one (ap) ; // ok
auto_ptr<Obj> two = one; //ok


unique_ptr<Obj> ap(new Obj() );
unique_ptr<Obj> one (ap) ; // 会出错
unique_ptr<Obj> two = one; //会出错
也就是说unique_ptr对对象的引用比较专一,不允许随随便便的进行转移
ii)  可以进行移动构造和移动赋值操作

3. shared_ptr
auto_ptr和unique_ptr都只能一个智能指针引用对象,而shared_ptr则是可以多个智能指针同时拥有一个对象。
shared_ptr实现方式就是使用引用计数。这一技术在COM中是用来管理COM对象生命周期的一个方式。
引用计数的原理是,多个智能指针同时引用一个对象,每当引用一次,引用计数加一,每当智能指针销毁了,引用计数就减一,
当引用计数减少到0的时候就释放引用的对象。这种引用计数的增减发生在智能指针的构造函数,复制构造函数,赋值操作符,析构函数中。
这种方式使得多个智能指针同时对所引用的对象有拥有权,同时在引用计数减到0之后也会自动释放内存,也实现了auto_ptr和unique_ptr的资源释放的功能。

4. weak_ptr
shared_ptr是一种强引用的关系,智能指针直接引用对象。

在stl中,weak_ptr是和shared_ptr配合使用的,在实现shared_ptr的时候也就考虑了weak_ptr的因素。
weak_ptr是shared_ptr的观察者,它不会干扰shared_ptr所共享对象的所有权,
当一个weak_ptr所观察的shared_ptr要释放它的资源时,它会把相关的weak_ptr的指针设置为空,防止weak_ptr持有悬空的指针。
注意:weak_ptr并不拥有资源的所有权,所以不能直接使用资源。
可以从一个weak_ptr构造一个shared_ptr以取得共享资源的所有权。

28、shared_ptr中的循环引用怎么解决?(weak_ptr)

在stl中,weak_ptr是和shared_ptr配合使用的,在实现shared_ptr的时候也就考虑了weak_ptr的因素。
weak_ptr是shared_ptr的观察者,它不会干扰shared_ptr所共享对象的所有权,
当一个weak_ptr所观察的shared_ptr要释放它的资源时,它会把相关的weak_ptr的指针设置为空,防止weak_ptr持有悬空的指针。
注意:weak_ptr并不拥有资源的所有权,所以不能直接使用资源。
可以从一个weak_ptr构造一个shared_ptr以取得共享资源的所有权。

29、vector与list比较

 

 

30、vector迭代器失效的情况

迭代器失效分三种情况考虑,也是非三种数据结构考虑,分别为数组型,链表型,树型数据结构。

数组型数据结构:该数据结构的元素是分配在连续的内存中,insert和erase操作,都会使得删除点和插入点之后的元素挪位置,所以,插入点和删除掉之后的迭代器全部失效,也就是说insert(*iter)(或erase(*iter)),然后在iter++,是没有意义的。解决方法:erase(*iter)的返回值是下一个有效迭代器的值。 iter =cont.erase(iter);

链表型数据结构:对于list型的数据结构,使用了不连续分配的内存,删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器.解决办法两种,erase(*iter)会返回下一个有效迭代器的值,或者erase(iter++).

树形数据结构: 使用红黑树来存储数据,插入不会使得任何迭代器失效;删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器.erase迭代器只是被删元素的迭代器失效,但是返回值为void,所以要采用erase(iter++)的方式删除迭代器。

注意:经过 erase(iter) 之后的迭代器完全失效,该迭代器 iter 不能参与任何运算,包括 iter++,*ite

31、map与unordered_map对比

 unordered_map和map类似,都是存储的key-value的值,可以通过key快速索引到value。不同的是unordered_map不会根据key的大小进行排序,

存储时是根据key的hash值判断元素是否相同,即unordered_map内部元素是无序的,而map中的元素是按照二叉搜索树存储,进行中序遍历会得到有序遍历。

所以使用时map的key需要定义operator<。而unordered_map需要定义hash_value函数并且重载operator==。但是很多系统内置的数据类型都自带这些,

那么如果是自定义类型,那么就需要自己重载operator<或者hash_value()了。

结论:如果需要内部元素自动排序,使用map,不需要排序使用unordered_map

32、set与unordered_set对比

1、set基于红黑树实现,红黑树具有自动排序的功能,因此map内部所有的数据,在任何时候,都是有序的。

2、unordered_set基于哈希表,数据插入和查找的时间复杂度很低,几乎是常数时间,而代价是消耗比较多的内存,无自动排序功能。底层实现上,使用一个下标范围比较大的数组来存储元素,形成很多的桶,利用hash函数对key进行映射到不同区域进行保存。

 set与unordered相比:

  1、set比unordered_set使用更少的内存来存储相同数量的元素。

  2、对于少量的元素,在set中查找可能比在unordered_set中查找更快。

  3、尽管许多操作在unordered_set的平均情况下更快,但通常需要保证set在最坏情况下有更好的复杂度(例如insert)。

  4、如果您想按顺序访问元素,那么set对元素进行排序的功能是很有用的。

  5、您可以用<、<=、>和>=从字典顺序上比较不同的set集。unordered_set集则不支持这些操作。

 

一般来说,在如下情况,适合使用set:

  1、我们需要有序的数据(不同元素)。

  2、我们必须打印/访问数据(按排序顺序)。

  3、我们需要知道元素的前任/继承者。

一般来说,在如下情况,适合使用unordered_set:

  1、我们需要保留一组元素,不需要排序。

  2、我们需要单元素访问,即不需要遍历。

  3、仅仅只是插入、删除、查找的话。

33、STL容器空间配置器

1、什么是空间配置器?

  空间配置器负责空间配置与管理。配置器是一个实现了动态空间配置、空间管理、空间释放的class template。以内存池方式实现小块内存管理分配。关于内存池概念可以点击:内存池。

2、STL空间配置器产生的缘由

  在软件开发,程序设计中,我们不免因为程序需求,使用很多的小块内存(基本类型以及小内存的自定义类型)。在程序中动态申请,释放。这个过程过程并不是一定能够控制好的,于是乎出现以下问题:

问题1:就出现了内存碎片问题。(ps外碎片问题)

问题2:一直在因为小块内存而进行内存申请,调用malloc,系统调用产生性能问题。

:内碎片:因为内存对齐/访问效率(CPU取址次数)而产生 如 用户需要3字节,实际得到4或者8字节的问题,其中的碎片是浪费掉的。

  外碎片:系统中内存总量足够,但是不连续,所以无法分配给用户使用而产生的浪费。如图:

这篇关于c++基础知识点(三)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!