文章目录
C++ 随机数
伪随机数概念:https://blog.csdn.net/mantou_riji/article/details/123445407
C++11 随机数
获取系统时间
转载:https://blog.csdn.net/weixin_39445116/article/details/122473569
通过C库的接口
#include <iostream>
#include <sstream>
#include <ctime>
int main() {
std::time_t t = std::time(nullptr); // 获取1970年到现在的秒数, 单位秒/s
std::cout << t << std::endl;
struct std::tm* now = std::localtime(&t); // 把毫秒转换为日期时间的结构体
std::stringstream timeStr;
// 以下依次把年月日的数据加入到字符串中
timeStr << now->tm_year + 1900 << "-";
timeStr << now->tm_mon + 1 << "-";
timeStr << now->tm_mday << " ";
timeStr << now->tm_hour << ":";
timeStr << now->tm_min << ":";
timeStr << now->tm_sec;
std::cout << timeStr.str();
return 0;
}
结果:
1698826244
2023-11-1 16:10:44
通过C++库接口
C++ 标准库提供了一些函数来获取当前时间。其中最常用的是 std::chrono::system_clock::now() 函数。它返回当前时间点的时间戳,以自 1970 年 1 月 1 日 00:00:00 UTC 起经过的秒数和纳秒数的组合形式表示。
#include <iostream>
#include <chrono>
int main()
{
auto now = std::chrono::system_clock::now();
auto timestamp = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
std::cout << "Current timestamp: " << timestamp << std::endl;
return 0;
}
内联inline关键字
内联函数的概念及目的
在C++中,inline是一个用于函数定义的修饰符,格式是直接在函数返回类型的最前面加上这个inline,产生效果是建议编译器将整个函数体代码段插入到每个调用点然后展开,从而消除函数调用产生的栈空间开销(代码转移到函数对应的内存地址执行,会记忆转移前的地址,反复调用就会转移多次),inline直接将函数嵌入到函数开始调用的那块。
关键字inline必须与函数定义放在一起才能使函数成为内联函数,仅仅将inline放在函数声明前面不起任何作用。inline是一种“用于实现”的关键字,而不是一种“用于声明”的关键字。
inline return_type function_name(parameter list) {
// 函数体
}
使用inline关键字的主要目的是为了提高函数的执行效率。当函数体较小且被频繁调用时,内联函数通常能带来性能上的提升。这是因为内联函数可以避免函数调用的开销,包括参数传递、堆栈的创建和销毁等。然而,需要注意的是,inline是一个对编译器的建议,编译器可以选择忽略这个建议,特别是当长且复杂(如大量循环等)的内联函数会导致嵌入后的代码膨胀反而影响性能。
inline内联函数的工作原理
内联函数不是在调用时发生控制转移关系,而是在编译阶段将函数体嵌入到每一个调用该函数的语句块中,编译器会将程序中出现内联函数的调用表达式用内联函数的函数体来替换。
普通函数是将程序执行转移到被调用函数所存放的内存地址,当函数执行完后,返回到执行此函数前的地方。转移操作需要保护现场,被调函数执行完后,再恢复现场,该过程需要较大的资源开销。
inline仅是一个对编译器的建议
inline 函数仅仅是一个对编译器的建议,所以最后能否真正内联,看编译器的意思,它如果认为函数不复杂,能在调用点展开,就会真正内联,并不是说声明了内联就会内联,声明内联只是一个建议而已。
类成员函数默认是内联类型的
类的成员函数可以被inline定义成内联函数。
对于在类体内部定义的成员函数,其实默认就是inline的,此时成员函数一般都省略写inline关键字。
然而,是否内联还是取决于编译器,编译器可能会根据函数的大小和其他因素来决定是否将函数内联。
class A
{
public:void Foo(int x, int y) { } // 自动地成为内联函数
}
但应当特别留意的是,若成员函数在类体外部定义而非内部定义,那么系统并不会默认将其视为内联(inline)函数。
这意味着在调用这些成员函数时,会遵循与普通函数调用相同的机制,包括可能的压栈和跳转开销。
如果希望将这些成员函数指定为内联函数,以期望获得性能上的优化,则必须显式地在函数声明前使用inline关键字进行声明。
这样做可以指导编译器在可能的情况下将函数体直接插入到调用点,从而消除函数调用的开销,提高程序执行的效率。
// 头文件
class A
{
public:
void Foo(int x, int y); // 函数声明时未定义,这种情况默认不是内联函数
}
// 函数定义文件
inline void A::Foo(int x, int y){} // 函数定义时使用inline关键字将函数定义为内联函数
inline 是一种"用于实现的关键字"
关键字 inline 必须与函数定义体放在一起才能使函数成为内联,仅将 inline 放在函数声明前面不起任何作用。
如下风格的函数 Foo 不能成为内联函数:
inline void Foo(int x, int y); // inline 仅与函数声明放在一起
void Foo(int x, int y){}
而如下风格的函数 Foo 则成为内联函数:
void Foo(int x, int y);
inline void Foo(int x, int y) {} // inline 与函数定义体放在一起
所以说,inline 是一种"用于实现的关键字",而不是一种"用于声明的关键字"。一般地,用户可以阅读函数的声明,但是看不到函数的定义。尽管在大多数教科书中内联函数的声明、定义体前面都加了inline 关键字,但我认为inline不应该出现在函数的声明中。这个细节虽然不会影响函数的功能,但是体现了高质量C++/C 程序设计风格的一个基本原则:声明与定义不可混为一谈,用户没有必要、也不应该知道函数是否需要内联。
inline的缺点
尽管内联函数可以提高性能,但它们也有一些缺点和限制:
代码膨胀:如果内联函数体很大,或者函数被频繁调用,那么将函数体插入到每个调用点可能会导致代码体积显著增加,从而可能降低指令缓存的效率,反而影响性能。
编译时开销:内联函数可能导致编译时间增加,因为编译器需要处理更多的代码。
链接时问题:内联函数通常需要在头文件中定义,而不是在源文件中。这可能会导致多个定义的问题,除非正确地使用inline关键字和链接器选项来避免这些问题。
编译器优化:编译器并不一定会内联所有标记为inline的函数。是否内联一个函数取决于多种因素,包括函数的大小、调用频率以及编译器的优化设置等。
因此,在决定是否使用内联函数时,需要权衡其带来的性能提升与可能带来的代码膨胀和编译时开销。
在大多数情况下,最好让编译器自动决定哪些函数应该内联,而不是显式地使用inline关键字。
inline内联与宏定义(define)的区别
内联函数与宏定义在C++编程中都有展开代码来提高代码执行效率的作用,它们区别如下:
(1)首先,从定义和性质上看,宏定义并非真正的函数,它在预处理阶段进行文本替换,即用宏体替换所有的宏名。
而内联函数本质则仍然是一种函数,它在编译时直接嵌入到目标代码中,替换了函数调用,从而消除了函数调用的开销。
内联函数具有普通函数所有的特性,比如有返回值、参数列表等,可以进行类型安全检查,而宏定义则没有这些特性。
宏定义只进行文本替换,不会对参数的类型、语句能否正常编译等进行检查。而内联函数是真正的函数,会对参数的类型、函数体内的语句编写是否正确等进行检查。
(2)从使用方式和调试角度看,宏定义在定义时需要小心处理宏参数,以避免出现二义性,而内联函数则不存在这个问题。
此外,由于内联函数是函数,因此它可以进行调试,而宏定义则不能。
(3)从作用范围上看,内联函数作为类的成员函数时,可以访问类的所有成员(公有、保护、私有),而宏定义则不能。
(4)在代码展开方面,虽然宏定义和内联函数都实现了代码的直接插入,但它们的处理时机不同。
宏定义在预处理阶段就完成了所有的替换工作,而内联函数则是在编译阶段进行插入。
这样的差异使得内联函数在效率提升的同时,还能确保代码的安全性和可读性。
通过避免函数调用的压栈和清栈开销,内联函数进一步提高了程序的执行效率
常考问题
问:既然内联函数在编译阶段已经在调用点被展开,那么程序运行时,对应的内存中不包含内联函数的定义,对吗?
错。
首先,如第一点所言,编译器可以选择调用内联函数,而非展开内联函数。因此,内存中仍然需要一份内联函数的定义,以供调用。
而且,一致性是所有语言都应该遵守的准则。普通函数可以有指向它的函数指针,那么,内联函数也可以有指向它的函数指针,因此,内存中需要一份内联函数的定义,使得这样的函数指针可以存在。
问:既然内联函数可以大大减少由函数调用带来的开销,提高程序的运行效率。那为什么所有的函数不都被设计成内联?
因为 内联是以代码膨胀复制为代价 ,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码, 将使程序的总代码量增大,消耗更多的内存空间。
以下情况不宜使用内联:
1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
以下情况适宜使用内联:
对于只有几条语句的小函数来说,与函数的调用、返回有关的准备和收尾工作的代码往往比函数体本身的代码要大得多。
因此,对于这类简单的、使用频繁的小函数,将之说明为内联函数可提高运行效率。
参考
C++基础整理(7)之关键字 inline(内联函数)
C++ Inline关键字
C++ 中的 inline 用法
卫语句
func
类型转换:static_cast、dynamic_cast、const_cast、reinterpret_cast
using关键字
C+±关键字:using
C++ using的三种用法详解
功能1:引入命名空间
将一个命名空间中的所有名字全部引入到当前作用域(将命名空间在当前作用域展开)。可能会存在命名冲突的问题
using namespace std;
也可以引入某个命名空间的单个名字到当前命名空间。
using std::cout;
using std::cin;
注意:位于头文件的代码一般来说不应该使用using声明。因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个using声明,那么每个使用了该头文件的文件就都会有这个声明,有可能产生名字冲突。
功能2:改变派生类对父类成员的访问控制
class Base{
protected:
int bn1;
int bn2;
};
class Derived: private Base{ //私有继承(省略继承方式时,默认是私有继承)
public:
using Base::bn1; //在当前作用域中引入了父类的保护成员, 在当前作用域中可以访问
};
class DerivedAgain: public Derived{ // 在Derived的子类中仍然可以访问bn1
};
int main(){
Derived d;
DerivedAgain da;
d.bn1 = 1;
d.bn2 = 2; //error, 'bn2' is a private member of 'Base'
da.bn1 = 3; //ok
std::cout<<d.bn1<<std::endl;
return 0;
}
————————————————
版权声明:本文为CSDN博主「wuqiongjin」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_51696091/article/details/127976813
尽管Derived是Base的私有继承,但是通过using声明父类的成员,我们就可以在Derived类以及其后续派生类中使用了。
功能3:类型重定义(代替typedef)
typedef std::vector<int> MyVector;
using MyVector = std::vector<int>;
using的写法把别名的名字强制分离到了左边,而把别名指向的放在了右边,比较清晰,可读性比较好。
另外,相比传统的typedef语法,using对于模板类的别名定义更友好,总结来看模板起别名时推荐使用using,而不是typedef。具体代码示例参考链接。
构造函数、析构函数、拷贝构造、移动构造、拷贝赋值、移动赋值
【C++】C++11-类的新功能&default&delete&final&override关键字
这篇参考文档中关于移动构造、移动赋值函数的自动生成条件描述不准确,以本文档中描述为准(都是通过自己编写类代码验证过的)。
【C++11保姆级教程】移动构造函数(move constructor)和移动赋值操作符(move assignment operator)
类中的默认成员函数
“默认”指的是开发者不显式声明和定义这些函数,编译器会自动生成。
C++98中有6个,C++11中新增2个共8个,分别为(假设类名为Base):
Base(); // 默认构造函数,不带参数
~Base(); // 默认析构函数,不带参数
Base(const Base &tmp); // 拷贝构造函数
Base &operator= (const Base &tmp); // 拷贝赋值函数
Base(const Base &&tmp); // 移动构造函数,C++11新增
Base &operator= (const Base &&tmp); // 移动赋值函数,C++11新增
// 取地址重载函数:不常用;
// const取地址重载函数:不常用;
默认函数的生成条件:
1. 默认构造函数:
当用户没有自己定义任何类型的构造函数,包括无参、带参构造函数,以及拷贝构造、移动构造函数,编译器才会自动生成默认的无参构造函数;
注意:如果用户自定义了拷贝构造、移动构造,也会导致编译器不会声明默认的构造函数。
2. 默认析构函数:
当用户没有自己定义析构函数时,编译器才会自动生成默认的析构函数;
3. 拷贝构造函数、拷贝赋值函数:
前提条件:用户必须没有自定义移动构造和移动赋值函数。
注意:只要定义了任何一个移动类型的函数,就会导致拷贝构造和拷贝赋值都不会自动生成,不是分别影响的。
当用户没有自己定义拷贝构造函数,编译器会自动生成拷贝构造;
当用户没有自己定义拷贝赋值函数,编译器会自动生成拷贝赋值;
如果自定义了拷贝构造函数,不会影响编译器自动生成另一者;反之亦然。
用户自定义一般的构造函数(包括带参的和无参的),不影响编译器自动生成拷贝构造和拷贝赋值函数。
4. 移动构造函数、移动赋值函数:
移动构造函数的生成条件:用户没有自己实现移动构造函数;
移动赋值函数的生成条件:用户没有自己实现移动赋值函数;
默认生成的移动构造和移动赋值函数作用
默认生成的移动构造函数:对于内置类型的成员会完成值拷贝(浅拷贝),对于自定义类型的成员,如果该自定义成员实现了移动构造就调用它的移动构造,如果没有,就调用它的拷贝构造函数。
默认生成的移动赋值重载函数:对于内置类型的成员会完成值拷贝(浅拷贝),对于自定义类型的成员,如果该自定义成员实现了移动赋值就调用它的移动赋值,如果没有写,就调用它的拷贝赋值函数。
调用时机:
Base base1;
Base base2(std::move(base1)); // 调用移动构造函数
Base base3 = std::move(base1); // 调用移动赋值函数
类成员变量的初始化
对于默认生成的构造函数:自定义类型的成员会调用其构造函数进行初始化,但并不会对内置类型的成员进行处理。
C++11支持非静态成员变量在声明时进行初始化赋值,默认生成的构造函数会使用这些缺省值对成员进行初始化。
class Person
{
public:
//...
private:
//非静态成员变量.可以在成员声明时给缺省值
Mango::string _name = "张三"; //姓名
int _age = 20; //年龄
static int _n; //静态成员变量不能给缺省值
};
注意:这里不是初始化,而是给声明的成员变量一个缺省值,真正的初始化是在初始化列表当中!
default、delete关键字
default
C++11可以让我们更好的控制要使用的默认成员函数,假设在某些情况下我们需要使用某个默认成员函数,但是因为某些原因导致无法生成这个默认成员函数,这时可以使用default关键字强制生成某个默认成员函数。
class Person
{
public:
Person() = default; //强制生成默认构造函数
//拷贝构造函数
Person(const Person& p)
:_name(p._name), _age(p._age)
{}
private:
Mango::string _name; //姓名
int _age; //年龄
};
此外,用default声明函数与空函数实现两种方式,还有代码执行效率方面之间的区别,具体可以看上面参考文档。
delete
当需要限制某些默认函数生成时,可以通过如下两种方式:
- 在C++98中,可以将该函数设置成私有,并且只用声明不用定义。这样当外部调用该函数时就会报错。
- 在C++11中,可以在该函数声明后面加上=delete,表示让编译器不生成该函数的默认版本。我们将=delete修饰的函数称为删除函数。
说明:被=delete修饰的函数可以设置为公有,也可以设置为私有,效果都一样。
final与override关键字
final
final修饰类:被final修饰的类叫做最终类,最终类无法被继承。
class NonInherit final //被final修饰.该类不能再被继承
{
//...
};
final修饰虚函数:表示该虚函数不能再被重写,如果子类继承后重写了该虚函数则编译报错。
//父类
class Person
{
public:
virtual void Print() final //被final修饰.该虚函数不能再被重写
{
cout << "hello Person" << endl;
}
};
//子类
class Student : public Person
{
public:
virtual void Print() //如果子类重写了Print函数,编译报错
{
cout << "hello Student" << endl;
}
};
override
override修饰虚函数: override修饰子类的虚函数,检查子类是否重写了父类的某个虚函数,如果没有没有重写则编译报错。
//父类
class Person
{
public:
virtual void Print()
{
cout << "hello Person" << endl;
}
};
//子类
class Student : public Person
{
public:
virtual void Print() override //检查子类是否重写了父类的某个虚函数
{
cout << "hello Student" << endl;
}
};
void * 指针
C语言–指针之空指针(void *)
[精选]C++:指针:void*指针(跳跃力未定的指针)
智能指针:auto_ptr、unique_ptr、shared_ptr、weak_ptr
(先看这个)C++ 智能指针 - 全部用法详解
(知乎高质量文档)智能指针详细解析(智能指针的使用,原理分析)
总结:
1、为什么要使用智能指针?
2、auto_ptr
auto_ptr 基于排他所有权模式:两个指针不能指向同一个资源,复制或赋值都会改变资源的所有权。
2.1 常见函数:
拷贝构造、拷贝赋值函数:新的智能指针托管动态内存,旧的智能指针失去托管(也就是智能指针对象中的指针变量会被置为空指针,如果用户再继续调用旧的智能指针,会触发空指针异常)。这也是C++11后auto_ptr已经被抛弃的主要原因,已经由unique_ptr替代。
get():获取智能指针托管的指针地址。
release():取消智能指针对动态内存的托管,但是不会释放原先托管的动态内存。
reset(T *):重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉,接管新的。
2.2 auto_ptr被C++11抛弃的原因:
(1)拷贝赋值或者拷贝构造都会改变资源的所有权,导致旧的智能指针对象不可用,容易引发空指针异常;
(2)在STL容器中使用auto_ptr存在着重大风险,因为容器内的元素必须支持可复制和可赋值;
(3)不支持对象数组的内存管理;
3、unique_ptr
- 基于排他所有权模式:两个指针不能指向同一个资源。
- 无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值:禁用了拷贝构造和拷贝赋值,但是允许移动构造和移动赋值。
- 在 STL 容器中使用unique_ptr,不允许直接赋值,如果非要赋值,需要使用move操作,使程序员知道后果。
- 支持对象数组的内存管理,并且在析构函数中会自动调用delete []释放内存。
auto_ptr 与 unique_ptr智能指针的内存管理陷阱
auto_ptr<string> p1;
string *str = new string("智能指针的内存管理陷阱");
p1.reset(str); // p1托管str指针
{
auto_ptr<string> p2;
p2.reset(str); // p2接管str指针时,会先取消p1的托管,然后再对str的托管
}
// 此时p1已经没有托管内容指针了,为NULL,在使用它就会内存报错!
cout << "str:" << *p1 << endl;
这是由于auto_ptr 与 unique_ptr的排他性所导致的。
4、shared_ptr
支持多个智能指针变量共享同一个内存地址,通过引用计数的方式来决定什么时候才会去真正地释放内存。
调用use_count()函数可以获得当前托管指针的引用计数。
循环引用导致无法释放资源问题:
解决方案:使用weak_ptr指向shared_ptr,不会导致shared_ptr中的引用计数加1。
5、weak_ptr
作用:协助shared_ptr工作,目的是解决循环引用场景下shared_ptr无法释放资源的问题。
它只可以从shared_ptr和weak_ptr对象构造。
构造和析构不会引起引用计数的变化。
没有重载*和->运算符。
在必要的时候可以通过lock()函数获得对应的shared_ptr共享指针对象,使用完之后,再将共享指针置NULL即可;
6、智能指针的使用陷阱
- 不要把一个原生指针同时给多个智能指针管理,如果其中一个指针释放了资源,会导致另一个智能指针变为野指针;
- 记得使用releae()返回的地址,因为该函数并不会释放内存,如果没有使用返回值进行释放内存就会导致内存泄漏;
- 禁止delete智能指针get函数返回的指针,会让智能指针变为野指针;
- 禁止用任何类型智能指针get函数返回的指针去初始化另一个智能指针,原因同1。
7、删除器
智能指针支持自定义删除器来释放内存,可以是函数指针、仿函数、lambda表达式、包装器等。
仿函数
先通过这个了解:C++之仿函数
再看这两个:
STL — 六. 仿函数 Functors
C++仿函数
union 联合体
C/C++中的联合体union介绍
联合体(union)的使用方法及其本质
C/C++语言中联合体union的妙用
模板
虚继承
C++语法——详解虚继承
清晰易懂介绍继承、访问控制、虚函数继承、多重继承:深入理解C++继承:访问控制、虚函数机制和多重继承详解
虚函数、虚表、纯虚函数、抽象类:C++虚函数详解
C++多态性介绍,函数重写、重载区别,虚函数与纯虚函数:【C++三大特性之多态】
虚函数
什么是虚函数?
虚函数是在基类中以关键字virtual说明,并在派生类中重新定义的一个非静态成员函数。
- 基类的虚函数在派生类中仍然是虚函数。
- 在派生类中重定义继承成员虚函数时,即使没有保留字virtual,该函数仍然是虚函数(为了提高程序的可读性,建议派生类中虚函数都加上virtual关键字)。
基类中的虚函数必须具有public或protected访问权限,且派生类必须以公有继承方式从基类派生。
虚函数的核心目的
虚函数使用的其核心目的是通过基类访问派生类定义的函数。
纯虚函数和抽象类
纯虚函数:在基类中没有具体实现的虚函数。
如果基类中包括有纯虚函数,那么在任何派生类中都必须重定义该函数,因为它们不能直接使用从基类继承下来的虚函数。
纯虚函数的一般形式为: virtual <函数返回类型> <函数名>(<参数表>) = 0;(花括号都没有)
包含纯虚函数的类称做抽象类。只要包含了都是抽象类,不能创建对象,只能创建指针或者引用。
由于无法实例化一个含纯虚函数的抽象类,因而不能创建抽象类的对象。
抽象类不能用作变量类型、函数返回和显式转换的类型,但可定义指向抽象类的指针或引用。
虚函数表,静态联编、动态联编
介绍虚函数概念和作用,虚函数表,静/动态联编:c++虚函数详解
1.为什么调用普通函数比调用虚函数的效率高?
2.为什么要用虚函数表(存函数指针的数组)?
3.为什么要把基类的析构函数定义为虚函数?
4.虚函数可以是内联的吗?
类型转换函数与explicit关键字
C++ 类型转换函数 与 explicit
C++ 中explicit关键字详解
const关键字
参考:
c++基础梳理(二):C++ 中的const关键字
C++ const 关键字小结
C++之const关键字详解
const 是 constant 的缩写,本意是不变的,不易改变的意思。
在 C++ 中用于定义常量。const起修饰作用,用来告诉编译器被修饰的对象是常量,不可以被修改。
1. 修饰普通类型的变量
const关键字用于定义一个常量,表示该量(变量或者对象)是不能被修改的,是只读变量。
注意:
- 定义时需要进行初始化(如果定义的是保证内容不变的const指针,则可以不在定义时初始化);
- const 变量有作用域,超过作用域不能使用;需要多个函数使用时,需要定义全局变量
- 跨多个cpp文件使用时,定义和引用的地方都要加上extern
- 跨h文件时,可以通过引用头文件使用
const int a = 10;
a = 11; // 错误,不可以被改变
const int a;
a = 10; // 错误,定义时需要初始化,不可以定义后再初始化
const int a = 10;
int *b = (int *)(&a);
*b = 11;
cout << a << endl; // 10
cout << *b << endl; // 11
//分析:编译器认为a的值不变,为一开始定义的10,因此在对a的实际内存地址操作后输出a还是10
// 如果想让编译器察觉到对a所做的操作,可以在const前面加上volatile关键字
volatile const int a = 10;
int *b = (int *)(&a);
*b = 11;
cout << a << endl; // 11
cout << *b << endl; // 11
//test5.cpp
extern const int var1 = 11; // 跨cpp文件使用的const变量
//test6.cpp
#include <iostream>
#include <string>
using namespace std;
extern const int var1;// 跨cpp文件使用的const变量
int main()
{
cout << "var1: " << var1 << endl;
}
//test7.h
const int var1 = 88;
//test7.cpp
#include <iostream>
#include <string>
#include "test7.h"
using namespace std;
int main()
{
cout << "var1: " << var1 << endl; // 跨h文件,通过引用头文件使用
}
2. 修饰指针
结论:左定值,右定向,const定义不变量。
const关键字在符号左边,保证指针指向的内容不可以被改变;const在符号右边,保证指针的指向不可以被改变。
// 类型1:左定值
int var1 = 10;
int var2 = 11;
const int * a; // 左定值,可以先不初始化
int const * b; // 正确,左定值的另一种写法
a = (int *)&var1;
*a = 11; // 错误,值不可以被改变
a = (int *)&var2; // 正确,指向可以被改变
// 类型2:右定向
int var1 = 10;
int var2 = 11;
int * const a = (int *) &var1; // 右定向,定义时候必须初始化
*a = 11; // 正确,值可以被改变
a = (int *)&var2; // 错误,指向不可以被改变
// 类型3:左定值+右定向
int var1 = 10;
int var2 = 11;
const int * const a = (int *) &var1; // 左定值+右定向,定义时候必须初始化
*a = 11; // 错误,值不可以被改变
a = (int *)&var2; // 错误,指向不可以被改变
3. 修饰自定义类型对象
被const修饰的自定义类型对象,表示该对象一旦被生成了,其成员变量将不会再被改变。
class MyClass {
public:
int a = 10;
};
int main2()
{
const MyClass obj;
cout << obj.a;
obj.a = 11; // 错误,obj被const修饰,不可以被改变
return 0;
}
4. 修饰成员函数
const 修饰类成员函数,表示该成员函数在执行过程中不会修改该对象的成员变量值。如果该函数实现中存在修改成员变量的行为,则编译过程会报错。
被const修饰的函数被称为“常函数”。
注意:const 关键字不能与 static 关键字同时使用,因为 static 关键字修饰静态成员函数,静态成员函数不含有 this 指针,即不能实例化,const 成员函数必须具体到某一实例。
如果有个成员函数想修改对象中的某一个成员怎么办?这时我们可以使用 mutable 关键字修饰这个成员,mutable 的意思也是易变的,容易改变的意思,被 mutable 关键字修饰的成员可以处于不断变化中。
- const成员函数,在函数访问方面,只能访问其他const函数,不能访问其他非const函数
- const成员函数,在变量访问方面,可以访问const和非const变量;
- 非const成员函数,可以访问const和非const的成员函数和变量;
- const 对象只能操作const成员函数,不能操作非成员函数;
- const对象可以访问const和非const变量
5. 修饰函数参数
1、值传递的const修饰,保证函数形参在函数调用过程中不能改变。(一般值传递的函数调用方式,形参的改变不会影响调用函数内实参的改变,因此这种const修饰没有意义)
2、指针传递的const修饰,保证指针的指向、内容不能被改变,具体原则还是参考左定值、右定向。
3、自定义类型对象的参数传递,如果采用值传递的参数传递方式,会造成对象的拷贝构造,比较浪费时间,因此我们采取 const 外加引用传递的方法。此时,const保证引用的对象在函数调用过程中不会被修改。
并且对于一般的 int、double 等内置类型,我们一般不采用引用的传递方式,因为内置类型对象生成不涉及构造函数调用,生成时间很快。
int test1(const int a) { // 值传递方式下的const修饰,一般没有意义
++a; // 错误,a被const修饰,不可以被改变
return a;
}
int test2(const int *a) { // 指针传递方式下的const修饰,左定值
int var1 = 10;
a = (int *) &var1; // 正确,左定值,方向可以被改变
*a = 10; // 错误,左定值,内容不可以被改变
return *a;
}
int test3(int * const a) { // 指针传递方式下的const修饰,右定向
int var1 = 10;
a = (int *) &var1; // 错误,右定向,方向不可以被改变
*a = 10; // 正确,右定向,内容可以被改变
return *a;
}
class MyClass {
// 类的实现省略
}
int test4(const MyClass &obj) { // 自定义类型,引用传递方式下的const修饰
obj.changeValue(); // 错误,const保证引用的对象在函数调用过程中不会被修改。
return 0;
}
6. 修饰函数返回值
1、当返回值为内置类型,被const修饰时,返回值内容不可以被修改,无意义。
2、当返回值为自定义类型对象,被const修饰时,此时返回的值不能作为左值使用,既不能被赋值,也不能被修改。
3、当返回值为指针时,被const修饰时,参考左定值右定向,此处不再赘述。
4、当返回值为自定义类型的引用时,被const修饰时,此时返回的值不能被修改。
extern “C”
extern “C” 是什么?作用是什么?
作用是实现类C语言和C++语言的混合编程,实现C和C++语言之间的互相调用。
C/C++程序开发一般分为:预编译、编译、汇编、链接,四个过程。
由于C语言和C++语言编译器对函数的处理方式不一样,导致针对相同函数(函数名、函数参数类型、个数、返回值等都相同)在编译产物(静态库、动态库)中的符号不一样。
具体表现在,C语言不存在函数重载用法,因此C编译器处理函数时只会通过函数名确定一个函数,编译产物中的函数符号中只有函数名的信息;而C++语言中存在函数重载的用法,因此C++编译器处理函数时会通过函数签名(函数名、参数类型、个数、返回值等)信息确定一个函数,因此编译产物中的函数符号跟C的不一样。
那么如果项目中出现了C和C++互相调用的场景,比如一个C++项目现在需要调用一个C实现的库中的一个函数,简单通过
#include “c_header.h”
包含头文件后通过函数名调用函数的方式,将会在项目编译后,链接的过程中报找不到该C函数符号的错误,原因就是C++编译器编译后该函数的符号跟C库中该函数的符号不一致。
这种问题的解决方法就是在C++程序中通过 extern “C” 向编译器声明被包含的代码属于C代码,让C++编译器使用C的编译方式去编译这些代码,这样编译出来的函数符号就和C库中的符号一致了。
这种问题主要出现在下面两个调用场景中:C++程序调用C写的库 和 C程序调用C++写的库。
C++程序调用C写的库
在C++代码中,当包含C库的头文件时,通过 extern “C” 向C++编译器声明该头文件属于C头文件,应该使用C编译方法编译。
extern "C" {
#include "c_header.h"
}
C程序调用C++写的库
在C++代码中,通过宏判断,如果当前属于C++编译环境,则为了保证编译出来的产物能够被C程序正确调用,会被调用的函数代码应该通过 extern “C” 包含,则编译C++库的时候,这部分代码就会按照C的方法去编译,之后被C项目引用后,这部分代码就会被正确调用。
#ifdefined(__cplusplus)||defined(c_plusplus) //跨平台定义方法
extern "C"{
#endif
//... 正常的声明段
#ifdefined(__cplusplus)||defined(c_plusplus)
}
#endif
#ifdef _cplusplus
该语句属于C++语言中的条件编译语句,其含义是:如果定义了宏 _cplusplus。
如果程序是由C++语言编写,由C++编译器编译,则该宏就会被默认定义。
因此,该条件编译语句其实就是确认当前编译环境是否是C++编译器环境。