例如,假设有一个类A,它有一个私有成员变量x和一个公有成员函数getX(),如果getX()返回x的引用,并且不加const修饰,那么就可以这样写:
A a;
a.getX() = 10; //修改了x的值
这样就破坏了类的封装性和数据安全性。但如果getX()返回x的引用,并且加上const修饰,那么就会报错
(1) const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。
而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应) 。
(2)有些集成化的调试工具可以对 const 常量进行调试,但是不能对宏常量进行调试。
(3)#define是在编译的预处理阶段起作用,而const是在编译、运行的时候起作用。
(4)#define定义的常量不分配内存,而const定义的常量会分配在常量存储区中。
经过内存对⻬之后,CPU 的内存访问速度⼤⼤提升。因为 CPU 把内存当成是⼀块⼀块的,块的⼤⼩可以是 2,4,8,16 个字节,因此 CPU 在读取内存的时候是⼀块⼀块进⾏读取的,块的大小称为内存读取粒度。⽐如说 CPU 要读取⼀个 4 个字节的数据到寄存器中(假设内存读取粒度是 4),如果数据是从 0 字节开始的,那么直接将 0-3 四个字节完全读取到寄存器中进⾏处理即可。
如果数据是从 1 字节开始的,就⾸先要将前 4 个字节读取到寄存器,并再次读取 4-7 个字节数据进⼊寄存器,接着把 0 字节,5,6,7 字节的数据剔除,最后合并 1,2,3,4字节的数据进⼊寄存器,所以说,当内存没有对⻬时,寄存器进⾏了很多额外的操作,⼤⼤降低了 CPU 的性能。
平台原因:不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
性能原因:应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次就可以了。
什么时候不希望进行内存对齐呢?一般来说,当我们追求空间效率而不是时间效率时,我们可以选择取消或者减小内存对齐。例如,在嵌入式系统中,由于资源有限,我们可能更关心节省空间而不是提高速度。此时我们可以使用编译器提供的选项来调整或者关闭内存对齐。
父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。
每个虚函数都会有一个与之对应的虚函数表,该虚函数表的实质是一个指针数组,存放的是每一个对象的虚函数入口地址。对于一个派生类来说,他会继承基类的虚函数表同时增加自己的虚函数入口地址,如果派生类重写了基类的虚函数的话,那么继承过来的虚函数入口地址将被派生类的重写虚函数入口地址替代。那么在程序运行时会发生动态绑定,将父类指针绑定到实例化的对象实现多态。每个类只有一个虚函数表,虚函数表是在编译的时候就确定的了。
用户不能创建基类的实例,只能创建派生类的实例
虚函数在运行时根据实际对象的类型来确定调用哪个函数,而不是根据指针或引用的类型来确定。当一个虚函数被定义为类的成员函数时,它会被标记为虚函数。在调用虚函数时,程序会查找该函数的实际类型,并在运行时调用该类型的实现。这就允许程序在运行时动态地选择执行哪个版本的虚函数,从而实现多态性。虚函数通常与基类指针或引用一起使用,可以实现基类指针或引用调用派生类的函数。
虚函数表是一个存储虚函数指针的数组,每个类有一个虚函数表,每个对象有一个指向虚函数表的指针。虚函数表的大小取决于类中有多少个虚函数,而对象中的虚函数表指针的大小取决于编译器和操作系统2。一般来说,在32位系统下,指针占4个字节,在64位系统下,指针占8个字节。
构造函数:构造函数不能被声明为虚函数。因为构造函数是用来创建对象的,而虚函数是根据对象的类型来动态调用的。如果构造函数是虚函数,那么在创建对象时就无法确定调用哪个版本的构造函数,会导致逻辑错误
友元函数:你说得对,构造函数不能被声明为虚函数。因为构造函数是用来创建对象的,而虚函数是根据对象的类型来动态调用的。如果构造函数是虚函数,那么在创建对象时就无法确定调用哪个版本的构造函数,会导致逻辑错误
普通函数:普通函数只能被重载,不能被重写
因为模板函数在编译时会被实例化为多个不同的函数,而虚函数需要在运行时才能确定调用哪个函数。在C++中,虚函数的实现依赖于虚函数表(vtable)和虚函数指针(vptr),而这些在编译时就需要确定下来。因此,虚函数不能是模板函数。
虚函数表不能放在全局区,因为全局区是存放全局变量和静态变量的,而虚函数表不是变量,而是一组指向类成员函数的指针。如果放在全局区,会导致内存浪费和混乱。
混乱:虚函数表是在编译期就确定了大小和内容的,而全局区是在运行期才分配空间的。如果把虚函数表放在全局区,就需要在运行期动态地为每个类分配空间,并且要保证不同类之间不会发生冲突。这样就增加了程序的复杂度和出错的可能性
解决方法:将两个父类添加成virtual,相当于爷爷直接拿出指针给孙子
static_cast 用于执行非多态类型之间的类型转换,例如整型和浮点型之间的转换、基类和派生类之间的指针或引用转换、void 指针和其他指针类型之间的转换等。该转换在编译时完成,通常不会检查运行时错误。
dynamic_cast 用于在运行时进行多态类型的转换。它通常用于将基类指针或引用转换为派生类指针或引用,以及在类层次结构中进行下行转换
dynamic_cast和虚函数的区别
reinterpret_cast 用于在不同的指针类型之间进行转换,**例如将一个指针转换为一个整数,或将一个整数转换为一个指针。**该转换通常不进行类型检查,因此潜在地不安全,只应在极少数特殊情况下使用。
const_cast 用于在去除变量的 const 修饰符或 volatile 修饰符时使用。它可以将指向常量对象的指针或引用转换为指向非常量对象的指针或引用,或者将指向非常量对象的指针或引用转换为指向常量对象的指针或引用。
在 C++ 中,关键字 volatile 用于声明一个变量是易变的(volatile variable),即该变量可能会在程序中的任意时刻被意外地改变。这意味着,当读取一个易变的变量时,编译器不会从缓存中读取该变量的值,而是每次都会从内存中重新读取该变量的值。同样地,当写入一个易变的变量时,编译器也不会将该变量的值存储在缓存中,而是立即将该变量的值写入内存中。
A* p = new B;
delete p;(A是父类 B是子类)
如果不定义虚析构 那么删除P只调用A的析构
定义为虚析构之后,删除P就会调用AB的析构
在C++中,当一个对象被复制时,其内部资源通常会被复制,这可能会导致性能问题,特别是在处理大型数据结构时。
移动语义是指,当一个对象被移动而不是复制时,其内部资源可以被“窃取”,而不需要进行复制操作。移动操作比复制操作更高效,因为它不需要分配新的内存或复制现有的内存。
可以,但是必须使用成员初始化列表为引用变量初始化,构造函数的形参也必须是引用类型
模板类是什么时候实现的
显式是程序员指定什么样的类型实现什么样的函数,后者是编译器来决定什么类型
当模板使用某种类型类型实例化后生成的类或函数不能满足需要时,可以考虑对模板进行具体化。具体化时可以修改原模板的定义,当使用该类型时,按照具体化后的定义实现,具体化相当于对某种类型进行特殊处理。
简单来讲 就是解决多态问题
模板的声明和定义不能分开写的原因是,编译器在编译模板时需要知道模板的完整定义,才能根据具体的类型参数生成相应的代码。如果只有声明而没有定义,编译器就无法生成正确的代码,链接时就会出现未定义引用的错误。
法一:一种是在声明文件中包含定义文件
// template.h
template <typename T>
void swap(T& a, T& b);
#include "template.cpp"
// template.cpp
template <typename T>
void swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
这样做的好处是可以保持声明和定义的分离,但缺点是每次修改定义文件都需要重新编译所有包含声明文件的源文件。
法二:在定义文件中显式实例化需要用到的类型参数
// template.h
template <typename T>
void swap(T& a, T& b);
// template.cpp
template <typename T>
void swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
// explicit instantiation for int and double types
template void swap<int>(int&, int&);
template void swap<double>(double&, double&);
这样做的好处是可以避免重复编译和代码膨胀,但缺点是需要提前知道所有可能用到的类型参数,并且每增加一个类型参数都需要修改定义文件
模板特化是指为某些特定类型提供不同于通用模板的具体实现
全特化是指为所有的模板参数都指定具体类型或值的情况
偏特化是指只为部分的模板参数指定具体类型或值,或者限制参数的范围。偏特化只适用于类模板,不适用于函数模板
一种情况是,如果模板的定义和声明都放在头文件中,并且在多个源文件中被包含和使用,那么编译器会为每个源文件生成相同的模板实例化代码。这样会导致目标文件中存在重复的代码段,增加了目标文件的大小,并且可能引起链接错误。为了避免这种情况,可以使用 extern 关键字来声明一个外部模板,在一个源文件中显式地实例化该模板,并且在其他源文件中只引用该实例化。
另一种情况是,如果模板的定义放在一个源文件中,并且在其他源文件中被包含和使用,那么编译器会根据不同的类型参数生成不同的模板实例化代码。这样可以避免重复的代码段,但是也可能导致链接错误,因为其他源文件无法找到该源文件中定义的模板。为了避免这种情况,可以将模板的声明放在头文件中,并且在需要使用该模板的源文件中包含该头文件
父类构造函数–>成员类对象构造函数–>自身构造函数
(当一个类的成员是另一个类的对象时,这个对象就叫成员对象.)
特点:
vector 的第二个模板形参是分配器(allocator),用于分配和管理 vector 内部存储元素的内存。分配器可以控制内存分配的策略,例如内存池等。如果不指定分配器,默认使用 std::allocator。
分配器通常是一个模板类,提供了 allocate 和 deallocate 等成员函数来分配和释放内存。在 vector 内部,使用分配器来分配和释放存储元素的内存,可以方便地替换默认的内存分配器,实现自定义的内存分配策略。
如果调用resize函数使得vector的大小变小了,那么后面的元素会被析构掉,也就是会调用元素类的析构函数。如果调用resize函数使得vector的大小变大了,那么新添加的元素会调用元素类的默认构造函数进行初始化,而不会调用析构函数。
在进行 vector 扩容时,如果存储的是自定义类型,会挨个复制构造元素,可能会造成性能问题。为了避免这一点,可以使用移动语义来优化。
在 C++11 引入的移动语义中,我们可以通过 std::move() 函数将一个对象转化为右值引用,这样就可以在元素的拷贝构造函数中实现移动语义,将对象的资源所有权从一个对象转移到另一个对象中,而不是进行深拷贝。
特性:
特性:
特性:
自动排序,稳定
查找,插入,删除都是O(logn)
umap底层是哈希表
哈希表适合小数据,查找插入删除最好都是O(1),最坏O(n),缺点是容易发生哈希冲突,设计哈希函数也比较困难
红黑树适合大数据集,但是代码实现较为复杂
在C++ STL中,空间配置器便是用来实现内存空间(一般是内存,也可以是硬盘等空间)分配的工具,他与容器联系紧密,每一种容器的空间分配都是通过空间分配器alloctor实现的。
开辟内存一般分为两步,一步是用构造函数,一部分用malloc或者new,前者直接在函数调用栈开辟空间,而后者先在堆里开辟空间,再隐式调用构造函数
关于内存空间的配置与释放,SGI STL采用了两级配置器:一级配置器主要是考虑大块内存空间,利用malloc和free实现;二级配置器主要是考虑小块内存空间而设计的(为了最大化解决内存碎片问题,进而提升效率),采用链表free_list来维护内存池(memory pool),free_list通过union结构实现,空闲的内存块互相挂接在一块,内存块一旦被使用,则被从链表中剔除,易于维护。
顺序容器使用删除会使后面的迭代器失效(自动往前进一,导致地址全变,所以会失效),解决办法:it=earse(it),即返回删除元素下一个的迭代器
关联容器map由于内部是红黑树,使用erase不会失效,但是需要记录一下下一个元素的迭代器,list使用上面两种方法都行
和指针的区别:
迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,重载了指针的一些操作符,–>、++、–等。迭代器封装了指针,是一个”可遍历STL( Standard Template Library)容器内全部或部分元素”的对象,本质是封装了原生指针,是指针概念的一种提升,提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,–等操作。
迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用取值后的值而不能直接输出其自身
迭代器相对于指针的优点在于,它提供了一些安全性和抽象性的保证。例如,如果你使用一个指向数组元素的指针,你可以对它进行任何操作,包括越界访问和非法修改等操作,这可能会导致内存错误和程序崩溃。而如果你使用一个vector迭代器,则可以避免这些问题,因为迭代器会自动检查越界和非法操作,并在出错时抛出异常或者进行其他处理。
介绍概念:capacity:该值在容器初始化时赋值,指的是容器能够容纳的最大的元素的个数。还不能通过下标等访问,因为此时容器中还没有创建任何对象。
size指的是此时容器中实际的元素个数。可以通过下标访问0-(size-1)范围内的对象。
resize即修改capacity大小,也修改size大小
reserve只修改capcaity大小
resize既分配了空间,也创建了对象;reserve表示容器预留空间,但并不是真正的创建对象,需要通过insert()或push_back()等创建对象。
给动态库函数传递容器的对象本身,则会出现内存堆栈破坏的问题。
产生问题的原因,容器和动态链接库相互支持不够好,动态链接库函数中使用容器时,参数中只能传递容器的引用,并且要保证容器的大小不能超出初始大小,否则导致容器自动重新分配,就会出现内存堆栈破坏问题。
如果要将一个临时变量push到容器的末尾,push_back()需要先构造临时对象,再将这个对象拷贝到容器的末尾,而emplace_back()则直接在容器的末尾构造对象,这样就省去了拷贝的过程。
vector:开辟三倍内存,旧数据开辟到新内存,释放旧的内存,指向新内存
std::shared_ptr是一种共享式智能指针,它可以让多个shared_ptr实例同时拥有同一个内存资源。shared_ptr内部维护了一个计数器,记录当前有多少个shared_ptr实例共享同一块内存。只有当计数器变为0时,才会自动释放内存。因此,shared_ptr可以避免多个指针指向同一块内存时出现的内存泄漏和悬空指针等问题。
std::unique_ptr是一种独占式智能指针,它可以保证指向的内存只被一个unique_ptr实例所拥有。当unique_ptr被销毁时,它所拥有的内存也会被自动释放。unique_ptr还支持移动语义,因此可以通过std::move来转移拥有权。
使用release()方法来移交指向的对象
用来解决shared_prt相互引用冲突的结果
举个一个不太恰当的例子,A和B相互加了微信,假设我们用一个指针来指向自己的微信朋友,如果是shared_ptr,那么A和B的生命周期是相互影响的,而实际上我们并不希望这种强绑定,比如假设B注销了账户,A根本不用知道,只有当A想发消息给B的时候系统才会发出提示:您还不是该用户的朋友。这时候weak_ptr就派上用场了。这也就是weak_ptr的第一种使用场景:
当你想使用对象,但是并不想管理对象,并且在需要使用对象时可以判断对象是否还存在
可以通过expired()函数来判断一个weak_ptr是否已经失效,如果expired()返回true,则表示它指向的对象已经被销毁或释放了。另外,使用lock()函数获取weak_ptr指向的对象时,如果返回的是一个空的shared_ptr,也可以判断weak_ptr是否已经失效。
[capture] (parameters) mutable ->return-type{statement}
中括号 “[]” 表示Lambda表达式的捕获列表,用于指定Lambda表达式访问外部作用域中的变量的方式。捕获列表可以为空,或者包含一个或多个捕获项,
int a = 1;
auto lambda = [a](int x, int y) -> int { return a + x + y; };
在这个例子中,捕获列表包含一个捕获项 “a”,表示Lambda表达式将访问外部作用域中的变量 “a”。
标识重载的 () 操作符的参数,没有参数时,这部分可以省略。参数可以通过按值(如: (a, b))和按引用 (如: (&a, &b))
两种方式进行传递。
[](int x, int y) -> int { return x + y; }
圆括号 “()” 表示Lambda表达式的参数列表,可以包含零个或多个参数。在这个例子中,Lambda表达式有两个参数,分别是一个整数 “x” 和一个整数 “y”
这部分可以省略。按值传递函数对象参数时,加上 mutable
修饰符后,可以修改传递进来的拷贝(注意是能修改拷贝,而不是值本身)。exception
声明用于指定函数抛出的异常,如抛出整数类型的异常,可以使用 throw(int)。
返回值类型:标识函数返回值的类型,当返回值为 void,或者函数体中只有一处 return
的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略
标识函数的实现,这部分不能省略,但函数体可以为空。
当定义一个Lambda表达式时,编译器会生成一个与Lambda表达式对应的新的(未命名的)函数对象类型和该类型的一个对象。这个函数对象可以重载函数调用运算符(),从而具有类似函数的行为。
圆括号传参数是通过函数调用运算符()来实现的。
当你使用圆括号传递参数给一个lambda表达式时,实际上是调用了它生成的函数对象的函数调用运算符(),并将参数传递给它。
函数调用运算符()会根据lambda表达式的定义来执行相应的代码,并返回一个值(如果有的话)。
所以,你可以把圆括号传参数看作是一种调用函数对象的方式,它让你不需要知道函数对象的名字或者类型就可以使用它。
方括号捕获外部变量(闭包)是通过将外部变量作为函数对象的成员来实现的。
当你在方括号中指定一个外部变量时,编译器会为你生成一个函数对象类型,它包含了这个外部变量作为它的一个成员。
当你创建一个函数对象时,这个成员会被初始化为外部变量的值或者引用,这取决于你是用=还是&来捕获它。
当你调用函数对象时,这个成员就可以在lambda表达式中使用,就像一个普通的局部变量一样。
所以,你可以把方括号捕获外部变量看作是一种创建闭包的语法糖,它让你不需要显式地定义一个类或者接口来保存外部变量的状态。
int x = 3;
auto lambda = [&] { return x * x; };
int result = lambda(); // result = 9
需要注意的是,对于值捕获和隐式捕获,Lambda表达式在创建时会复制一份外部变量的值到闭包中,如果在Lambda表达式中修改闭包中的变量值,不会影响外部变量的值。而对于引用捕获,Lambda表达式会直接操作外部变量,可以改变其值。
右值引用是C++11引入的一种引用类型,它用于表示临时对象和即将销毁的对象
int num = 10;
int &b = num; //正确
int &c = 10; //错误
int num = 10;
//int && a = num; //右值引用不能初始化为左值
int && a = 10;
和常量左值引用不同的是,右值引用还可以对右值进行修改。例如:int && a = 10;
a = 100;
cout << a << endl;
移动语义是C++11引入的一项重要特性,它允许将一个对象的资源所有权从一个对象转移到另一个对象,而不需要进行昂贵的复制操作。
移动语义通过将右值引用传递给移动构造函数和移动赋值运算符,使得可以将一个临时对象或者即将销毁的对象的资源转移到一个新的对象中,从而避免了进行昂贵的复制操作。
std::vector<int> vec1 = {1, 2, 3};
std::vector<int> vec2 = std::move(vec1);
move函数
int x = 10;
int&& r = move(x); //将左值强制转换为右值
转发是指在函数中将参数按照原始的类型和值,转发给另一个函数。
常规转发是指将参数通过传值或引用的方式传递给另一个函数,这是C++中的传参方式。但是在传递参数时,会存在一些问题。例如,当我们想把一个右值参数传递给一个函数时,我们可能会遇到编译器错误。另一个例子是当我们要将一个右值参数转发给一个函数时,但是我们不知道该使用传值还是传引用,因为这个决定取决于被调用的函数的定义。为了解决这些问题,C++11 引入了完美转发。
完美转发是指将参数以原始的类型和值传递给另一个函数,并保留其右值或左值特性。这可以通过使用转发引用(forwarding reference)和 std::forward 函数来实现。转发引用是一种通用引用,它可以引用任何类型的值,并且可以保留值的右值或左值特性。当我们使用转发引用作为函数的参数时,我们可以在函数内部使用 std::forward 来将参数转发给另一个函数,以保留参数的右值或左值特性。
#include
#include
void foo(int n) {
std::cout << "foo(int): " << n << std::endl;
}
void bar(const int& n) {
std::cout << "bar(const int&): " << n << std::endl;
}
template<typename T>
void callFoo(T&& arg) { //常规转发
foo(arg);
}
template<typename T>
void callBar(T&& arg) {常规转发
bar(arg);
}
// 完美转发
void foo2(int n) {
std::cout << "foo2(int): " << n << std::endl;
}
void bar2(const int& n) {
std::cout << "bar2(const int&): " << n << std::endl;
}
template<typename T>
void callFoo2(T&& arg) {
foo2(std::forward<T>(arg));
}
template<typename T>
void callBar2(T&& arg) {
bar2(std::forward<T>(arg));
}
int main() {
int n = 42;
// 常规转发
callFoo(n); // 输出 foo(int): 42
callBar(n); // 输出 bar(const int&): 42
// 完美转发
callFoo2(n); // 输出 foo2(int): 42
callFoo2(std::move(n)); // 输出 foo2(int): 42
callBar2(n); // 输出 bar2(const int&): 42
callBar2(std::move(n)); // 输出 bar2(const int&): 42
return 0;
}
在常规转发中,参数被传递给了被调用函数,但是它们的类型都是引用类型,因此在函数内部处理时,不会进行任何类型转换。也就是说,如果我们传递给常规转发的参数是一个左值,那么被调用函数内部处理时,它们依然是左值引用;如果我们传递的是一个右值,那么它们依然是右值引用。这样可能会导致一些效率问题,比如如果被调用函数需要将参数进行复制,那么这个过程可能会比较耗时。
而在完美转发中,我们使用了转发引用 T&&,并在函数内部使用了 std::forward 函数来进行类型转换。这样可以保留参数的原有类型特性,使得被调用函数内部处理时,参数的类型会根据传递给它的参数类型进行调整,从而避免了一些不必要的类型转换操作。此外,通过完美转发,我们还可以保留参数的右值特性,避免了一些额外的复制操作,从而提高了代码的效率。
静态全局变量:静态全局变量在声明它的整个文件中都是可见的,而在文件之外是不可见的;(作用域是整个文件)变量的生存周期存在于整个程序运行期间。
静态局部变量:内存存放在程序的全局数据区中,静态局部变量在程序执行到该对象声明时,会被首次初始化。其后运行到该对象的声明时,不会再次初始化(只会被初始化一次),变量的生存周期存在于整个程序运行期间。
静态函数(主要目的确定作用域):作用域只在声明它的文件当中,不能被其他文件引用,其他文件可以定义同名的全局函数,其他文件想要调用本文件的静态函数,需要显示的调用extern关键字修饰其声明。
静态成员变量:用于修饰 class 的数据成员,即所谓“静态成员”。这种数据成员的生存期大于 class 的对象(实体 instance)。静态数据成员是每个 class 有一份,普通数据成员是每个 instance 有一份,因此静态数据成员也叫做类变量,而普通数据成员也叫做实例变量。诞生比构造函数早,在类声明的时候就产生了。
静态成员函数:静态成员函数不能访问非静态(包括成员函数和数据成员),但是非静态可以访问静态
对于C语言的全局和静态变量,初始化发生在任何代码执行之前,属于编译期初始化。
而C++标准规定:全局或静态对象当且仅当对象首次用到时才进行构造(静态全局和静态局部)。
然而,静态成员变量与静态局部变量和全局变量不同。它们必须在类的外部进行初始化,并且在程序开始执行之前就已经被分配内存并初始化了。
内联函数是在 C++ 中增加的一个功能,可以提高程序执行效率。如果函数是内联的,编译器在编译时,会把内联函数的实现替换到每个调用内联函数的地方,可以与宏函数作类比,但宏函数不会进行类型检查。
引入内联函数主要是解决一些频繁调用的小函数消耗大量空间的问题。
通常情况下,在调用函数时,程序会将控制权从调用程序处转移到被调用函数处,在这个过程中,传递参数、寄存器操作、返回值等会消耗额外的时间和内存,如果调用的函数代码量很少,也许转移到调用函数的时间比函数执行的时间更长。而如果使用内联函数,内联函数会在调用
如果使用很多内联函数,生成的二进制文件会变大;
编译的时间会增加,因为每次内联函数有修改,就需要重新编译代码。
所以,并不是所有函数都要声明为内联函数,需要视具体情况而定。
宏函数是在预编译的时候把所有的宏名用宏体来替换,简单的说就是字符串替换 ;而内联函数则是在编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这样可以省去函数的调用的开销,提高效率
宏定义是没有类型检查的,无论对还是错都是直接替换;而内联函数在编译的时候会进行类型检查,内联函数满足函数的性质,比如有返回值、参数列表等
编译器在编译期间完成的,编译器会根据实参类型来推断该调用哪个函数,如果有对应的函数,就调用,没有则在编译时报错。
动态绑定是指在运行时确定函数的实际调用函数。如果一个函数被声明为虚函数,那么在运行时就可以使用动态绑定,使得调用正确的实现。这种绑定通常是通过虚函数表(Virtual Table)来实现的,虚函数表是一个存储指向虚函数地址的指针数组,每个包含虚函数的类都有一个虚函数表。例如调用 ptr->speak() 时,根据指针实际指向的对象类型进行动态绑定,调用相应的 speak() 实现
增强程序的可扩充性,即程序需要修改或增加功能时,只需改动或增加较少的代码。简化代码,使得不同的子类对象都可以使用同一个名称的函数,而具有不同的实现。实现动态绑定,即在运行时根据对象的实际类型来调用相应的虚函数。
小端:78 56 34 12(低位在低字节)
大端:12 34 56 78(低位在高字节)
1
引用和引用变量共同占一个空间,可以说,指针看的是地址,引用看的是变量本身,所以引用更加安全(不能取到引用本身的地址。如果去取引用的地址,编译器会帮你变成去所指向变量的地址123。所以对引用取地址,其实取到的是所指向的值的地址)
浅拷贝只复制指针,新旧两个东西共享同一块内存,当对象拥有动态分配的内存时,使用浅拷贝可能会导致资源泄露或内存访问错误
深拷贝会创建一个新的对象,包括内存,这意味着每个对象都有自己独立的内存副本,即使一个对象被改变,另一个也不会受影响
struct默认公有继承,class默认私有继承
struct内不能声明函数,class可以
函数A();
函数B();
Bfuc(A);
B就是回调
意义:因为传进来的函数是不确定的。可以传函数a,也可以传函数b,直接在函数体里面调用就写死了
当new申请的是C++对象数组时,delete和delete []差别就很大了,delete只会析构一个对象
delete和delete[]的区别主要在于是否调用析构函数。如果用delete[],则在回收空间之前所有对象都会首先调用自己的析构函数。基本类型的对象没有析构函数,所以回收基本类型组成的数组空间用delete和delete[]都是应该可以的;但是对于类对象数组,只能用delete[]。否则可能会造成内存泄漏或者其他错误。
存储
函数重载的关键是函数的参数列表——也称为函数特征标(function signature)。如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同,而变量名是无关紧要的。C++允许定义名称相同的函数,条件是它们的特征标不同。如果参数数目和或参数类型不同,则特征标也不同。
在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。
使用函数指针来实现,重载的函数不能使用同名称,只是类似的实现了函数重载功能
向上转型(Upcasting)是指将一个派生类的指针或引用转换为它的基类的指针或引用的过程。这种转型是安全的,因为一个派生类对象也是一个基类对象,基类指针或引用可以指向派生类对象。
向下转型(Downcasting)是指将一个基类的指针或引用转换为它的派生类的指针或引用的过程。这种转型是不安全的,因为一个基类对象可能不是一个派生类对象,如果对其进行向下转型,可能会导致未定义的行为或内存错误。向下转型应该尽可能避免使用,除非可以确定基类对象是派生类对象。
在 C++ 中,构造函数可以调用虚函数,但是要注意一些细节。在构造函数中调用虚函数时,实际调用的是当前正在构造的对象的虚函数,而不是派生类中重写的虚函数。这是因为在执行派生类的构造函数之前,基类的构造函数会先被执行,此时派生类的对象尚未构造完成,因此调用派生类中的虚函数是不安全的。
同上,在析构函数中调用虚函数时,也会按照当前对象的类型来执行,而不是动态绑定到基类的实现
Base constructor
Base virtual method
Derived constructor
可以看到,Base 的构造函数中调用了虚函数,但是实际执行的是 Base 类中的虚函数,而不是 Derived 类中的虚函数。因此,在构造函数中调用虚函数时,需要特别小心,以避免出现问题。
必须是引用,如果拷贝构造函数中的参数不是一个引用,即形如CClass(const CClass c_class),那么就相当于采用了传值的方式(pass-by-value),而传值的方式会调用该类的拷贝构造函数,从而造成无穷递归地调用拷贝构造函数。因此拷贝构造函数的参数必须是一个引用。
把函数当成类用
class SquareFunctor {
public:
int operator()(int x) {
return x * x;
}
};
SquareFunctor square;
int result = square(5); // 计算 5 的平方,结果为 25
类模板是模板的定义,不是一个实实在在的类,定义中用到通用类型参数
模板类是实实在在的类定义,是类模板的实例化。类定义中参数被实际类型所代替。
64位系统存一个地址的空间大小取决于内存地址的位数。一般来说,一个内存地址对应一个字节(8位),所以64位系统可以表示16个16进制数(64位)的内存地址。这样,64位系统的最大寻址空间为2的64次方字节,即16384PB或16777216TB。但是,并不是所有的64位系统都能使用这么大的寻址空间,因为有些CPU只有40位或48位的地址线,而且操作系统也有自己的限制。
在C++中,定义函数int function(int a[], int b),这里数组a不会在内存中拷贝,传递的是指针。数组名就是一个指向数组第一个元素的指针,所以当你把数组名作为参数传递时,实际上是传递了一个指针。如果你想要传递整个数组的副本,你可以使用引用或者复制数组的内容到另一个数组。
使用友元可以简化代码,提高效率。
举例来说,假设有一个类 A 和一个函数 F,A 中有一个私有成员变量 x,而 F 函数需要访问这个私有变量。如果不使用友元,就只能使用 A 的公有接口来获取 x,这样会增加代码的复杂性和开销。
链接库:C语言和C++语言使用的链接库不同,C语言使用C标准库,C++语言使用C++标准库。C++标准库中包含了C标准库中的所有函数,同时还包含了STL(标准模板库)和一些面向对象的特性,如命名空间、类、继承等。
#ifdef __cplusplus
// Code being compiled as C++.
#endif
这是一个预处理器指令,它可以让你在编译前对源代码进行一些操作。#ifdef __cplusplus 的意思是如果__cplusplus 宏被定义了,就执行后面的代码。__cplusplus 宏是一个特殊的宏,它只有在C++ 编译器下才会被定义,所以这个指令可以用来检查当前的编译环境是否是C++。#endif 的意思是结束#ifdef 的范围。
ISWow64Process函数
导致文件描述符结构中指针指向的内存被重复释放,进而导致一些不可预期的异常。
当数组名作为参数时,传递的实际上是地址。
而其他类型如int作为参数时,由于函数参数值实质上是实参的一份拷贝,被调函数内部对形参的改变并不影响实参的值。
全局对象的构造函数会在main 函数之前执行。
指针变量也占用内存单元,而且所有指针变量占用内存单元的数量都是相同的。32位4个字节,64位8个字节
就是说,不管是指向何种对象的指针变量,它们占用内存的字节数都是一样的,并且要足够把程序中所能用到的最大地址表示出来(通常是一个机器字长)。
数据类型 16位系统 32位系统 64位系统
bool 1字节 1字节 1字节
char 1字节 1字节 1字节
int 2字节 4字节 4字节
float 4字节 4字节 4字节
double 8字节 8字节 8字节
void 不占空间 不占空间 不占空间
wchar_t 2字节 2或4字节5 2或4或8字节5
C++继承时,一般要写类的构造函数(包括拷贝构造)、析构函数、赋值运算符重载函数,以及其他需要实现类的功能或接口的成员函数
只允许在栈上创建对象:
class MyClass {
public:
// ...
private:
MyClass() {} // 私有构造函数
friend class StackOnly; // 声明友元类
};
class StackOnly {
public:
StackOnly() {} // 默认构造函数
~StackOnly() {} // 默认析构函数
static void* operator new(std::size_t) = delete; // 禁用 new 运算符
static void* operator new[](std::size_t) = delete; // 禁用 new[] 运算符
};
int main() {
StackOnly stack;
MyClass obj; // 错误:不能在堆上创建对象
return 0;
}
只允许在堆上创建对象:
编译器在栈上创建对象时,如果类的析构函数是私有的,则无法回收栈上的内存。因此无法在栈上创建,所以可以将析构变成私有。这样做也有一个缺点,就是需要提供一个公有的静态成员函数来释放堆区对象的内存
只允许在内存池中创建对象:
class MyPool {
public:
static void* allocate(std::size_t size) {
// 自定义内存池分配内存
}
static void deallocate(void* ptr, std::size_t size) {
// 自定义内存池释放内存
}
};
class MyObject {
public:
void* operator new(std::size_t size) {
return MyPool::allocate(size);
}
void operator delete(void* ptr) {
MyPool::deallocate(ptr, sizeof(MyObject));
}
void* operator new[](std::size_t size) = delete;
void operator delete[](void* ptr) = delete;
};
在上面的示例中,MyObject 类重载了 operator new 和 operator delete
运算符,从而实现了只允许在内存池中创建对象的功能。operator new 运算符调用 MyPool::allocate
方法从内存池中分配内存,而 operator delete 运算符调用 MyPool::deallocate 方法释放内存。同时,禁用了
operator new[] 和 operator delete[]
运算符,从而防止在自由存储区创建数组对象。在实际使用时,需要根据具体的内存池实现来修改 MyPool 类中的代码。
RTTI就是运行时动态绑定
同时,编译器也会在每个有虚函数的类对应的type_info对象中存储该类的类型信息。type_info对象是一个标准库提供的类,它包含了类型名称、哈希码等信息,并且重载了==和!=运算符来比较两个类型是否相同。
为了让type_info对象和虚函数表关联起来,编译器会在虚函数表 的开头插入一个指针,指向当前类对应的type_info对象。这样,在运行时就可以通过基类指针p找到vfptr,再通过vfptr找到type_info对象指针,进而取得类型信息。
type_info信息存在虚函数表的开头,也就是虚函数表的第一个元素是一个指向type_info对象的指针 。
C++中,临时对象是编译器在不同的情况下创建的没有名字的对象。临时对象通常出现在以下场景:
引用初始化,例如 const int& r = 42;
参数传递,例如 f(42);
表达式求值,例如 a + b;
函数返回,例如 return x + y;
异常抛出,例如 throw x;
临时对象有一个生命周期,由它们的创建点和销毁点决定4。任何创建多个临时对象的表达式最终会按照创建的逆序销毁它们3。临时对象的销毁时间取决于它们的使用方式4:
用于初始化const引用的临时对象:如果引用是局部变量,则在引用离开作用域时销毁;如果引用是类成员,则在类实例被销毁时销毁。
用于初始化非const引用或值类型的临时对象:在表达式结束后立即销毁。
静态链接库(lib)是在编译时将库的代码直接复制到可执行文件中,所以在程序运行时不需要依赖任何外部库文件,所有的代码都在一个可执行文件中。因此,静态链接库的优点是移植方便,无需安装其他库文件,程序运行时速度较快。缺点是占用硬盘空间较大,同时也存在代码重复的情况,不利于代码的更新和维护。
动态链接库(dll)是在程序运行时才被加载到内存中,程序需要调用库函数时才会加载对应的库文件。因此,动态链接库的优点是共享库文件,节省了硬盘空间,同时也方便了库文件的更新和维护。缺点是相对于静态链接库,程序运行时会存在一定的额外开销,如加载库文件、解析符号等。
memcpy 的实现比较简单,它只是简单地把数据从源地址按字节逐一复制到目标地址。这意味着,如果源地址和目标地址存在重叠,即它们指向同一块内存区域,那么 memcpy 可能会出现未定义的行为,也就是数据会被错误地覆盖。因此,使用 memcpy 时需要确保源地址和目标地址不会发生重叠。
而 memmove 的实现则更为复杂,它能够处理源地址和目标地址重叠的情况,即使这两个地址相互重叠,也能够保证正确地复制数据。具体来说,memmove 在复制数据时会先把数据拷贝到一个临时缓冲区中,然后再把数据从缓冲区复制到目标地址,这样就避免了源地址和目标地址重叠时数据被错误地覆盖的问题。但是,由于要使用临时缓冲区,所以 memmove 的性能可能会稍微低一些。