sizeof(p),p 为指针得到的是一个指针变量的字节数,而不是p 所指的内存容量。对于修改函数中数据的函数:
static 作⽤:控制变量的存储⽅式和可⻅性
函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
在模块内的static全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问;
在模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;
在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;
在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。
static类对象必须要在类外进行初始化,static修饰的变量先于对象存在,所以static修饰的变量要在类外初始化;
由于static修饰的类成员属于类,不属于对象,因此static类成员函数是没有this指针的,this指针是指向本对象的指针。正因为没有this指针,所以static类成员函数不能访问非static的类成员,只能访问 static修饰的类成员;
static成员函数不能被virtual修饰,static成员不属于任何对象或实例,所以加上virtual没有任何实际意义;静态成员函数没有this指针,虚函数的实现是为每一个对象分配一个vptr指针,而vptr是通过this指针调用的,所以不能为virtual;虚函数的调用关系,this->vptr->ctable->virtual function
int *const p2中const修饰p2的值,所以理解为p2的值不可以改变,即p2只能指向固定的一个变量地址,但可以通过*p2读写这个变量的值。顶层指针表示指针本身是一个常量int const *p1或者const int *p1两种情况中const修饰*p1,所以理解为*p1的值不可以改变,即不可以给*p1赋值改变p1指向变量的值,但可以通过给p赋值不同的地址改变这个指针指向。底层指针表示指针所指向的变量是一个常量。int const *const p;浅拷贝 —-只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,我们把这种拷贝叫做“(浅复制)浅拷贝”,换句话说,浅复制仅仅是指向被复制的内存地址,如果原地址中对象被改变了,那么浅复制出来的对象也会相应改变。
深拷贝 —-在计算机中开辟了一块新的内存地址用于存放复制的对象。
在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。
简单的来说,【浅拷贝】是增加了一个指针,指向原来已经存在的内存。而【深拷贝】是增加了一个指针,并新开辟了一块空间让指针指向这块新开辟的空间。【浅拷贝】在多个对象指向一块空间的时候,释放一个空间会导致其他对象所使用的空间也被释放了,再次释放便会出现错误
编译器在编译的时候,编译器会为每个包含虚函数的类创建一个虚表(即vtable),该表是一个一维数组(而不是一个链表),在这个数组中存放每个虚函数的地址。
编译器另外还为每个带有虚函数的类的对象自动创建一个虚表指针(即vptr),这个指针指向了对象所属类的虚表。在程序运行时,根据对象的类型去初始化vptr,从而让vptr正确的指向所属类的虚表。所以在调用虚函数时,就能够找到正确的函数。
由于每个对象调用的虚函数都是通过虚表指针来索引的,也就决定了虚表指针的正确初始化是非常重要的。换句话说,在虚表指针没有正确初始化之前,我们不能够去调用虚函数。
那么虚表指针在什么时候,或者说在什么地方初始化呢?
答案是在构造函数中进行虚表的创建和虚表指针的初始化。
构造函数的调用顺序,在构造子类对象时,要先调用父类的构造函数,之后再完成子类的构造。在调用父类的构造函数时,编译器只“看到了”父类,并不知道后面是否后还有继承者,它初始化虚表指针,将该虚表指针指向父类的虚表。当执行子类的构造函数时,虚表指针被重新赋值,指向自身的虚表。
对于虚函数调用来说,每一个对象内部都有一个虚表指针,该虚表指针被初始化为指向本类的虚表。所以在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针是固定的,所以才能实现动态的对象函数调用。
this -> vptr -> vtable ->virtual function对于派⽣类来说,编译器建⽴虚函数表的过程其实⼀共是三个步骤:
这两种方式的主要区别在于:
C++的赋值操作是会产生临时对象的。临时对象的出现会降低程序的效率,所以成员初始化列表会快一些,效率更高。
编译器会一一操作初始化列表,以适当顺序在构造函数之内安插初始化操作,并且在任何显示用户代码前。list中的项目顺序是由类中的成员声明顺序决定的,不是初始化列表中的排列顺序决定的。
一个派生类构造函数的执行顺序如下:
① 虚拟基类的构造函数(多个虚拟基类则按照继承的顺序执行构造函数)。
② 基类的构造函数(多个普通基类也按照继承的顺序执行构造函数)。
③ 类类型的成员对象的构造函数(按照初始化顺序)
④ 派生类自己的构造函数。
析构函数相反。
从存储空间角度,虚函数相应一个指向虚函数表的指针,但是这个指向虚函数表的指针事实上是存储在对象的内存空间的。假设构造函数是虚的,就须要通过 虚函数表来调用,但是对象还没有实例化,也就是内存空间还没有,所以构造函数不能是虚函数。
C++中基类采用virtual虚析构函数是为了防止内存泄漏。
析构函数与构造函数的作用相反,用于撤销对象的一些特殊任务处理,可以是释放对象分配的内存空间;
特点:析构函数与构造函数同名,但该函数前面加~。 析构函数没有参数,也没有返回值,而且不能重载,在一个类中只能有一个析构函数。
当撤销对象时,编译器也会自动调用析构函数。 每一个类必须有一个析构函数,用户可以自定义析构函数,也可以是编译器自动生成默认的析构函数。一般析构函数定义为类的公有成员。
构造函数或者析构函数调用虚函数并不会发挥虚函数动态绑定的特性,跟普通函数没区别;
构造函数,构造函数初始化对象,派生类必须知道基类函数干了什么,才能进行构造;当有虚函数时,每一个类有一个虚表,每一个对象有一个虚表指针,虚表指针在构造函数中初始化;内联函数,内联函数表示在编译阶段进行函数体的替换操作,而虚函数意味着在运行期间进行类型确定,所以内联函数不能是虚函数;静态函数,静态函数不属于对象属于类,静态成员函数没有this指针,因此静态函数设置为虚函数没有任何意义。友元函数,友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法。普通函数,普通函数不属于类的成员函数,不具有继承特性,因此普通函数没有虚函数。从语法上看都可以抛出异常。
C++只会析构已经完成的对象,对象只有在其构造函数执行完毕才算是完全构造妥当。在构造函数中发生异常,控制权转出构造函数之外。因此,对象的构造函数中发生异常,对象的析构函数不会被调用。因此会造成内存泄漏。auto_ptr对象来取代指针类成员,便对构造函数做了强化,免除了抛出异常时发生资源泄漏的危机,不再需要在析构函数中手动释放资源;只能静态分配即只能在栈上创建对象。
是把new、delete运算符重载为private属性。
class A {
private:
void* operator new(size_t t){} //设置为私有
void operator delete(void* ptr){} //重载了new就需要重载delete。对应重载。
public:
A(){}
~A(){}
};
只有使用new运算符,对象才会被建立在堆上,因此只要限制new运算符就可以实现类对象只能建立在栈上。可以将new运算符设为私有。
只能动态分配即只能在堆上创建对象。
只允许动态分配需要禁止直接调用构造函数。
把构造、析构函数设为protected属性,再用子类来动态创建
A a;;A *p = new A();动态建立一个类对象,就是使用new运算符为对象在堆空间中分配内存。这个过程分为两步,第一步执行operator new()函数,在堆中搜索一块内存并进行分配;第二步调用类构造函数构造对象;派生类中包含并且可以使用它从基类继承而来的成员,为了使用这些成员,派生类必须知道他们是什么。
https://www.cnblogs.com/QG-whz/p/4676481.html
惟有默认构造函数”被需要“的时候编译器才会合成默认构造函数。
合成的默认构造函数中,只有基类子对象和成员类对象会被初始化。所有其他的非静态数据成员都不会被初始化。
类的对象需要拷⻉时,拷⻉构造函数将会被调⽤,以下的情况都会调⽤拷⻉构造函数:
拷贝构造:B(A)
拷贝赋值:B=A,B已经定义,只是赋值。
一个类里面的数据成员是另一个类的对象,即内嵌其他类的对象作为自己的成员;
创建组合类的对象:首先创建各个内嵌对象,难点在于构造函数的设计。创建对象时既要对基本类型的成员进行初始化,又要对内嵌对象进行初始化。
创建组合类对象,构造函数的执行顺序:先调用内嵌对象的构造函数,然后按照内嵌对象成员在组合类中的定义顺序,与组合类构造函数的初始化列表顺序无关。然后执行组合类构造函数的函数体,析构函数调用顺序相反。
一:继承
继承是Is a 的关系,比如说Student继承Person,则说明Student is a Person。
继承的优点是子类可以重写父类的方法来方便地实现对父类的扩展。
继承的缺点有以下几点:
①:父类的内部细节对子类是可见的。
②:子类从父类继承的方法在编译时就确定下来了,所以无法在运行期间改变从父类继承的方法的行为。
③:如果对父类的方法做了修改的话(比如增加了一个参数),则子类的方法必须做出相应的修改。所以说子类与父类是一种高耦合,违背了面向对象思想。
二:组合
组合也就是设计类的时候把要组合的类的对象加入到该类中作为自己的成员变量。
组合的优点:
①:当前对象只能通过所包含的那个对象去调用其方法,所以所包含的对象的内部细节对当前对象是不可见的。
②:当前对象与包含的对象是一个低耦合关系,如果修改包含对象的类中代码不需要修改当前对象类的代码。
③:当前对象可以在运行时动态的绑定所包含的对象。可以通过set方法给所包含对象赋值。
组合的缺点:①:容易产生过多的对象。②:为了能组合多个对象,必须仔细对接口进行定义。
https://blog.csdn.net/qq_43410906/article/details/107435448
抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层。
抽象类的定义:
称带有纯虚函数的类为抽象类。
抽象类的作用:
抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。所以派生类实际上刻画了一组子类的操作接口的通用语义,这些语义也传给子类,子类可以具体实现这些语义,也可以再将这些语义传给自己的子类。
原因:
抽象类尚未实现方法,所以不能创建对象。
创建对象去调用方法是指做明确的事情,而这个抽象方法并不明确,所以只有继承抽象类去实现抽象方法才可以。
指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。C++支持两种多态性:编译时多态性,运行时多态性。
有四种方式会调用析构函数:
生命周期:对象生命周期结束,会调用析构函数。
delete:调用delete时,会删除指针类对象。
包含关系:对象Dog是对象Person的成员,Person的析构函数被调用时,对象Dog的析构函数也被调用。
继承关系:当Person是Student的父类,调用Student的析构函数,会调用Person的析构函数。
友元函数可以直接访问类的私有成员和保护成员;因为编译器必须能够读取这个结构的声明以理解这个数据类型的大小、行为等方面的所有规则。
有一条规则在任何关系中都很重要,那就是谁可以访问我的私有部分。
https://blog.csdn.net/weixin_45722843/article/details/124440056
#include <iostream>
using namespace std;
//C++中的继承与多态
struct A
{
virtual void fun() //C++中的多态:通过虚函数实现
{
cout << "A:fun()" << endl;
}
int a;
};
struct B :public A //C++中的继承:B类公有继承A类
{
virtual void fun() //C++中的多态:通过虚函数实现(子类的关键字virtual可加可不加)
{
cout << "B:fun()" << endl;
}
int b;
};
//C语言模拟C++的继承与多态
typedef void (*FUN)(); //定义一个函数指针来实现对成员函数的继承
struct _A //父类
{
FUN _fun; //由于C语言中结构体不能包含函数,故只能用函数指针在外面实现
int _a;
};
struct _B //子类
{
_A _a_; //在子类中定义一个基类的对象即可实现对父类的继承
int _b;
};
void _fA() //父类的同名函数
{
printf("_A:_fun()\n");
}
void _fB() //子类的同名函数
{
printf("_B:_fun()\n");
}
void Test()
{
//测试C++中的继承与多态
A a; //定义一个父类对象a
B b; //定义一个子类对象b
A* p1 = &a; //定义一个父类指针指向父类的对象
p1->fun(); //调用父类的同名函数
p1 = &b; //让父类指针指向子类的对象
p1->fun(); //调用子类的同名函数
//C语言模拟继承与多态的测试
_A _a; //定义一个父类对象_a
_B _b; //定义一个子类对象_b
_a._fun = _fA; //父类的对象调用父类的同名函数
_b._a_._fun = _fB; //子类的对象调用子类的同名函数
_A* p2 = &_a; //定义一个父类指针指向父类的对象
p2->_fun(); //调用父类的同名函数
p2 = (_A*)&_b; //让父类指针指向子类的对象,由于类型不匹配所以要进行强转
p2->_fun(); //调用子类的同名函数
}
向上类型转换
将派生类指针或引用转换为基类的指针或引用被称为向上类型转换,向上类型转换会自动进行,而且向上类型转换是安全的。
向下类型转换
将基类指针或引用转换为派生类指针或引用被称为向下类型转换,向下类型转换不会自动进行,因为一个基类对应几个派生类,所以向下类型转换时不知道对应哪个派生类,所以在向下类型转换时必须加动态类型识别技术。用dynamic_cast进行向下类型转换。
https://blog.csdn.net/Jacky_Feng/article/details/120742414
在C++11中可以取地址的、有名字的就是左值,
反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。
举个例子,int a = b+c, a 就是左值,其有变量名为a,通过&a可以获取该变量的地址;表达式b+c、函数int func()的返回值是右值,在其被赋值给某一变量前,我们不能通过变量名找到它,&(b+c)这样的操作则不会通过编译。
左值引用,就是绑定到左值的引用,通过&来获得左值引用。
右值引用,就是绑定到右值的引用,通过&&来获得右值引用。
左值也可以作为右值表达式,变量可以是左值,也可以为右值,但常量只能是右值。
C++11之前右值只能被const 类型的引用所指向;而左值可以被const或非const类型引用指向
int num = 10;
const int &b = num;
const int &c = 10;
在C++11中右值又分为纯右值和将亡值。
- 纯右值的概念等同于我们在C++98标准中右值的概念,指的是临时变量和不跟对象关联的字面量值;
- 将亡值则是C++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移为他用),比如返回右值引用
T&&的函数返回值、std::move的返回值,或者转换为T&&的类型转换函数的返回值。将亡值可以理解为通过“盗取”其他变量内存空间的方式获取到的值。在确保其他变量不再被使用、或即将被销毁时,通过“盗取”的方式可以避免内存空间的释放和分配,能够延长变量值的生命期。
C++11 标准新引入了另一种引用方式,称为右值引用,用 “&&” 表示。
右值引用主要用于移动语义和完美转发,其中前者需要有修改右值的权限;其次,常量右值引用的作用就是引用一个不可修改的右值,这项工作完全可以交给常量左值引用完成。
和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化。
int num = 10;
//int && a = num; //右值引用不能初始化为左值
int && a = 10;
右值引用可以修改右值
int && a = 10;
a = 11;
cout << a << endl; //输出结果为11
move()函数将左值强制转换到右值
move(arg)
//agr:左值对象,该函数返回arg对象的右值形式
int num = 10;
int&& a = std::move(num); //编译成功
cout << a << endl; //输出结果为10;
总结如下:
非常量左值引用可以引用的值的类型只有非常量左值,常量左值引用非常量左值、常量左值及右值
int num = 10;
int& a = num; //编译成功,非常量左值引用支持引用非常量左值
const int num2 = 100;
int& b = num2; //编译失败,非常量左值引用不支持引用常量左值
int& c = 10; //编译失败,非常量左值引用不支持引用右值
const int& d = num; //编译成功,常量左值引用支持引用非常量左值
const int& e = num2; //编译成功,常量左值引用支持引用常量左值
const int& f = 100; //编译成功,常量左值引用支持引用右值
右值引用不支持引用左值;非常量右值引用可以引用的值的类型只有非常量右值,常量右值引用非常量右值、常量右值
int num = 10;
const int num2 = 100;
int&& a = num; //编译失败,非常量右值引用不支持引用非常量左值
int&& b = num2; //编译失败,非常量右值引用不支持引用常量左值
int&& c =10; //编译成功,非常量右值引用支持引用非常量右值
const int&& d = num; //编译失败,常量右值引用不支持引用非常量左值
const int&& e = num2; //编译失败,常量右值引用不支持引用常量左值
const int&& f = 100; //编译成功,常量右值引用支持引用右值
https://blog.csdn.net/weixin_36725931/article/details/85218924
移动构造函数是参数类型为右值引用的拷贝构造函数。
移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的初值是一个右值引用。
意味着,移动构造函数的参数是一个右值或者将亡值的引用。也就是说,只有当用一个右值,或者将亡值初始化另一个对象的时候,才会调用移动构造函数。而那个move语句,就是将一个左值变成一个将亡值。
//参数为左值引用的深拷贝构造函数,转移堆内存资源所有权,改变 source.ptr_ 的值
Integer(Integer& source)
: ptr_(source.ptr_) {
source.ptr_ = nullptr;
cout << "Call Integer(Integer& source)" << endl;
}
//移动构造函数,与参数为左值引用的深拷贝构造函数基本一样
Integer(Integer&& source)
: ptr_(source.ptr_) {
source.ptr_ = nullptr;
cout << "Call Integer(Integer&& source)" << endl;
}
Integer(int value)
: ptr_(new int(value)) {
cout << "Call Integer(int value)" << endl;
}
https://blog.csdn.net/qq_37753409/article/details/82026613
源代码-->预处理-->编译-->优化-->汇编-->链接-->可执行文件.
.i后缀为预处理后的c文件,.ii后缀为预处理后的C++文件。.s后缀为编译后的文件.o后缀为汇编后的目标文件o(1);但因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。另外,当数组中内存空间不够时,会重新申请一块内存空间并进行内存拷贝。o(n);但由于链表的特点,能高效地进行插入和删除。怎么找某vector或者list的倒数第二个元素
int mySize = vec.size();
vec.at(mySize -2);
list不提供随机访问,所以不能用下标直接访问到某个位置的元素,要访问list里的元素只能遍历,不过你要是只需要访问list的最后N个元素的话,可以用反向迭代器来遍历:

size()函数返回的是已用空间大小,capacity()返回的是总空间大小,capacity()-size()则是剩余的可用空间大小。
当size()和capacity()相等,说明vector目前的空间已被用完,如果再添加新元素,则会引起vector空间的动态增长。
由于动态增长会引起重新分配内存空间、拷贝原空间、释放原空间,这些过程会降低程序效率。因此,可以使用reserve(n)预先分配一块较大的指定大小的内存空间,这样当指定大小的内存空间未使用完时,是不会重新分配内存空间的,这样便提升了效率。只有当n>capacity()时,调用reserve(n)才会改变vector容量。
resize()成员函数只改变元素的数目,不改变vector的容量。
size()和capacity()都为0reserve()预先分配一块内存后,在空间未满的情况下,不会引起重新分配,从而提升了效率。reserve()分配的空间比原空间小时,是不会引起重新分配的。resize()函数只改变容器的元素数目,未改变容器大小。reserve(size_type)只是扩大capacity值,这些内存空间可能还是“野”的,如果此时使用“[ ]”来访问,则可能会越界。而resize(size_type new_size)会真正使容器具有new_size个对象。https://blog.csdn.net/qq_44918090/article/details/120583540
扩容机制回顾
当向vector中插入元素时,如果元素有效个数size与空间容量capacity相等时,vector内部会触发扩容机制:
注意:每次扩容新空间不能太大,也不能太小,太大容易造成空间浪费,太小则会导致频繁扩容而影响程序效率。
如何避免扩容导致效率低
如果要避免扩容而导致程序效率过低问题,其实非常简单:如果在插入之前,可以预估vector存储元素的个数,提前将底层容量开辟好即可。如果插入之前进行reserve,只要空间给足,则插入时不会扩容,如果没有reserve,则会边插入边扩容,效率极其低下。
为什么选择以倍数方式扩容
以倍数的方式扩容比以等长个数的扩容方式效率高。对比可以发现采用成倍方式扩容,可以保证常数的时间复杂度,而增加指定大小的容量只能达到O(n)的时间复杂度,因此,使用成倍的方式扩容。
为什么选择1.5倍或者2倍方式扩容,而不是3倍、4倍
https://www.cnblogs.com/biyeymyhjob/archive/2012/09/12/2674004.html
由于vector的内存占用空间只增不减,比如你首先分配了10,000个字节,然后erase掉后面9,999个,留下一个有效元素,但是内存占用仍为10,000个。所有内存空间是在vector析构时候才能被系统回收。
empty()用来检测容器是否为空的,clear()可以清空所有元素。但是即使clear(),vector所占用的内存空间依然如故,无法保证内存的回收。
如果需要空间动态缩小,可以考虑使用deque。如果vector,可以用swap()来帮助你释放内存。
vector<Point>().swap(pointVec); //或者pointVec.swap(vector<Point> ())
template < class T >
void ClearVector( vector< T >& vt )
{
vector< T > vtTemp;
veTemp.swap( vt );
}
swap()是交换函数,使vector离开其自身的作用域,从而强制释放vector所占的内存空间,总而言之,释放vector内存最简单的方法是vector<Point>().swap(pointVec)。当时如果pointVec是一个类的成员,不能把vector<Point>().swap(pointVec)写进类的析构函数中,否则会导致double free or corruption (fasttop)的错误,原因可能是重复释放内存。
顺序容器:顺序容器有以下三种:可变长动态数组 vector、双端队列 deque、双向链表 list。
erase迭代器不仅使所指向被删除的迭代器失效,而且使被删元素之后的所有迭代器失效(list除外),所以不能使用erase(it++)的方式,但是erase的返回值是下一个有效迭代器;
It = c.erase(it);
关联容器:关联容器有以下四种:set、multiset、map、multimap。
erase迭代器只是被删除元素的迭代器失效,但是返回值是void,所以要采用erase(it++)的方式删除迭代器;
c.erase(it++);
set底层是以红黑树(RB-Tree)实现,hash_set底层是以hash_table实现的;set和hash_set元素的键值就是实值;map内部会根据key的大小进行排序,内部使用的是红黑树实现hash_map其实叫作std::unordered_map,底层实现是hash table,hash_map不具有自动排序的功能;unordered_map存储时是根据key的hash值判断元素是否相同,而map中的元素是按照二叉搜索树存储,进行中序遍历会得到有序遍历。hash_map底层使用的是hash_table,hash_table使用的开链法进行冲突避免,所有hash_map采用开链法进行冲突解决。HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。用insert函数插入pair数据
map<int, string> mp;
mp.insert(pair<int,string>(1,"aaaaa"));
用insert函数插入value_type数据
map<int, string> mp;
mp.insert(map<int, string>::value_type(3,"ccccc"));
在insert函数中使用make_pair()函数
map<int, string> mp;
mp.insert(make_pair<int,string>(2,"bbbbb"));
用数组方式插入数据
map<int, string> mp;
mp[4] = "ddddd";
[]的作用是:将key作为下标去执行查找,并返回对应的值;如果不存在这个key,就将一个具有该key和值类型的默认值的项插入map中。key执行查找,找到了返回该位置的迭代器;如果不存在这个key,就返回尾迭代器。list不再能够像vector一样以普通指针作为迭代器,因为其节点不保证在存储空间中连续存在;list插入操作和结合操作都不会造成原有的list迭代器失效;list不仅是一个双向链表,而且还是一个环状双向链表,所以它只需要一个指针;list不像vector那样有可能在空间不足时做重新配置、数据移动的操作,所以插入前的所有迭代器在插入操作之后都仍然有效;deque是一种双向开口的连续线性空间,所谓双向开口,意思是可以在头尾两端分别做元素的插入和删除操作;可以在头尾两端分别做元素的插入和删除操作;deque和vector最大的差异,一在于deque允许常数时间内对起头端进行元素的插入或移除操作,二在于deque没有所谓容量概念,因为它是动态地以分段连续空间组合而成,随时可以增加一段新的空间并链接起来,deque没有所谓的空间保留功能。vector 底层数据结构为数组 ,支持快速随机访问list 底层数据结构为双向链表,支持快速增删deque 底层数据结构为一个中央控制器和多个缓冲区stack 底层一般用list或deque实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时queue 底层一般用list或deque实现,默认情况下,如果没有为queue实例化指定容器类,则使用标准容器deque。priority_queue 的底层数据结构一般为vector为底层容器,堆heap为处理规则来管理底层容器实现set 底层数据结构为红黑树,有序,不重复multiset 底层数据结构为红黑树,有序,可重复map 底层数据结构为红黑树,有序,不重复multimap 底层数据结构为红黑树,有序,可重复hash_set 底层数据结构为hash表,无序,不重复hash_multiset 底层数据结构为hash表,无序,可重复hash_map 底层数据结构为hash表,无序,不重复hash_multimap 底层数据结构为hash表,无序,可重复https://blog.csdn.net/zj1131190425/article/details/92065897
定义
函数指针是指向函数的指针变量。函数指针本身是⼀个指针变量,该指针变量指向⼀个具体的函数。
如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址,函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。
函数指针的声明方法
double (*pf)(int); // 指针pf指向的函数, 输入参数为int,返回值为double
为什么有函数指针
调⽤函数和做函数的参数,⽐如回调函数(要将函数作为参数进行传递,必须传递函数名)。
两种方法赋值:
指针名 = 函数名; 指针名 = &函数名
函数指针的使用示例:
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
double cal_m1(int lines)
{
return 0.05 * lines;
}
double cal_m2(int lines)
{
return 0.5 * lines;
}
void estimate(int line_num, double (*pf)(int lines))
{
cout << "The " << line_num << " need time is: " << (*pf)(line_num) << endl;
}
int main(int argc, char *argv[])
{
int line_num = 10;
// 函数名就是指针,直接传入函数名
estimate(line_num, cal_m1);
estimate(line_num, cal_m2);
return 0;
}
int fun(int ,int)经过名字修饰之后变为 _fun_int_int ,而C是 _fun,一般是这样的,所以C++才会支持不同的参数调用不同的函数;const_cast,static_cast,reinterpret_cast和dynamic_cast栈区(stack)— 由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。堆区(heap) — 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS(操作系统)回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。 一般存放new或malloc出来的对象全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。文字常量区 —常量字符串就是放在这里的。程序结束后由系统释放。程序代码区 —存放函数体的二进制代码。管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生内存泄漏。
空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的。
碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出。
生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca()函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,无需我们手工实现。
分配效率:
栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
野指针:不确定其具体指向的指针。“野指针”最常来自于未初始化的指针。
“野指针”的成因主要有3种:
① 指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。例如
char *p = NULL;
char *str = new char(100);
② 指针p被free或者delete之后,没有置为NULL;
③ 指针操作超越了变量的作用范围:不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。
如何避免野指针:
对指针进行初始化
①将指针初始化为NULL。
char * p = NULL;
②用malloc分配内存
char * p = (char * )malloc(sizeof(char));
③用已有合法的可访问的内存地址对指针初始化
char num[ 30] = {0};
char *p = num;
指针用完后释放内存,将指针赋NULL。
delete(p);
p = NULL;
野指针:野指针是不确定其具体指向的指针,一般指那些未初始化的指针。
悬空指针:一个指针的指向对象已经被删除,那么就成了悬空指针。
bac_alloc异常。malloc分配内存失败时返回NULL。int* p=new int[10],delete p和delete[] p作用效果是一样的,原因是内部普通数据类型没有析构函数。先调用operator new分配内存,然后在分配的内存上调用构造函数:new[]先调用operator new[]分配内存,然后在p的前四个字节写入数组大小n,然后调用n次构造函数,针对复杂类型,new[]会额外存储数组大小;步骤总结如下:
先调用析构函数再调用operator delete;假设指针p指向new[]分配的内存,因为要4字节存储数组大小,实际分配的内存地址为[p-4],系统记录的也是这个地址。delete[]实际释放的就是p-4指向的内存。而delete会直接释放p指向的内存,这个内存根本没有被系统记录,所以会崩溃。https://blog.csdn.net/qq_44844115/article/details/99433233
实际使用时,不要混用,因为不知道会发生什么未知的事情!!!
https://blog.csdn.net/Wuxin_20/article/details/89641981
https://blog.csdn.net/weibo1230123/article/details/81503135
相同点:
不同点:
函数参数类型不同;
calloc函数会对申请空间初始化,并且初始化为0;
void* calloc(size_t n,size_t size);
int *p = calloc(20, sizeof(int));//申请20个int类型的空间。
malloc函数申请的空间的值是随机初始化的,必须使用memset进行初始化;
void* malloc(unsigned int num_size);
int *p = malloc(20*sizeof(int));申请20个int类型的空间;
realloc函数用于对动态内存进行扩容(及已申请的动态空间不够使用,需要进行空间扩容操作),ptr为指向原来空间基址的指针, new_size为接下来需要扩充容量的大小。
void realloc(void *ptr, size_t new_Size)
https://blog.csdn.net/qq_34170700/article/details/107493939
C++11中主要提供三种智能指针:
shared_ptr 、 unique_ptr 、 weak_ptr。
auto_ptr是c++98的方案,c++11已经抛弃
申请的空间在函数结束时忘记释放,造成内存泄漏。<memory>中。make_shared函数或者通过构造函数传入普通指针。并可以通过get函数获得普通指针。引用成环例子:
#include <iostream>
#include <memory>
class CB;
class CA {
public:
CA() {
std::cout << "CA()" << std::endl;
}
~CA() {
std::cout << "~CA()" << std::endl;
}
void set_ptr(std::shared_ptr<CB>& ptr) {
m_ptr_b = ptr;
}
private:
std::shared_ptr<CB> m_ptr_b;
};
class CB {
public:
CB() {
std::cout << "CB()" << std::endl;
}
~CB() {
std::cout << "~CB()" << std::endl;
}
void set_ptr(std::shared_ptr<CA>& ptr) {
m_ptr_a = ptr;
}
private:
std::shared_ptr<CA> m_ptr_a;
};
int main()
{
std::shared_ptr<CA> ptr_a(new CA());
std::shared_ptr<CB> ptr_b(new CB());
ptr_a->set_ptr(ptr_b);
ptr_b->set_ptr(ptr_a);
std::cout << ptr_a.use_count() << " " << ptr_b.use_count() << std::endl;
return 0;
}
成员函数:
use_count 返回引用计数的个数。unique 返回是否是独占所有权( use_count 为 1)。swap 交换两个 shared_ptr 对象(即交换所拥有的对象)。reset放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少。get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr<int> sp(new int(1)); sp 与 sp.get()是等价的。make_shared 与 new 的区别:
new会导致内存碎片化,make_shared则不会。
new: 先new后赋值的方式,是先在堆上分配一块内存,然后在堆上再建一个智能指针控制块,这两个东西是不连续的,会造成内存碎片化;
make_shared: make_shared的方式是直接在堆上新建一块足够大的内存,其中包含两部分,上面是内存(用来使用),下面是控制块(包含引用计数),然后用T的构造函数去初始化分配的内存。
shared_ptr<int> p1 = make_shared<int>(42);//安全的内存分配返回指向此对象的shared_ptr
https://blog.csdn.net/qq_38410730/article/details/105903979
weak_ptr 是一种不影响对象生命周期的智能指针, 它指向一个shared_ptr管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。
weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。
weak_ptr设计的目的是为配合 shared_ptr而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr或另一个 weak_ptr对象构造, 它的构造和析构不会引起引用记数的增加或减少。
weak_ptr是用来解决shared_ptr相互引用时的死锁问题(引用成环)。它是对对象的一种弱引用,不会修改对象的引用计数,只能用来检测对象是否已经释放,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。
引用成环解决方法:以上文的例子来说,解决办法就是将两个类中的一个成员变量改为weak_ptr对象,比如将CB中的成员变量改为weak_ptr对象,即CB类的代码如下::
class CB {
public:
CB() {
std::cout << "CB()" << std::endl;
}
~CB() {
std::cout << "~CB()" << std::endl;
}
void set_ptr(std::shared_ptr<CA>& ptr) {
m_ptr_a = ptr;
}
private:
std::weak_ptr<CA> m_ptr_a;
};
在C++11之前,auto_ptr 的作用与 unique_ptr 类似:独占所指对象,采用所有权模式。
在C++11之后,auto_ptr已经被弃用。
auto_ptr的构造函数是explicit,阻止了一般指针隐式转换为 auto_ptr的构造,所以不能直接将一般类型的指针赋值给auto_ptr类型的对象,必须用auto_ptr的构造函数创建对象;
由于auto_ptr对象析构时会删除它所拥有的指针,所以使用时避免多个auto_ptr对象管理同一个指针;
auto_ptr内部实现,析构函数中删除对象用的是delete而不是delete[],所以auto_ptr不能管理数组;
auto_ptr与unique_ptr的区别在于:
如果尝试对 auto_ptr/unique_ptr 进行拷贝或赋值,unique_ptr 会直接在编译阶段报错;而auto_ptr却可以编译通过,只有在运行阶段,且访问到由于被拷贝而导致失去了对象控制权后变成空指针的auto_ptr后,程序才会报错。
auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.”));
auto_ptr<string> p2;
p2 = p1; //auto_ptr不会报错.
此时不会报错,p2剥夺了p1的所有权,所有权转让,p1已经不指向该对象,指向空。当程序运行时访问p1将会报错。
所以auto_ptr的缺点是:存在潜在的内存崩溃问题!
unique_ptr 独占其所指对象,保证同一时间内只有一个智能指针可以指向该对象。
属于跟踪引用和所有权模式。
没有一个类似于make_shared的函数用于初始化unique_ptr,unique_ptr只能绑定到一个new返回的指针上。
unique_ptr<string> p3 (new string ("auto")); //#4
unique_ptr<string> p4; //#5
p4 = p3;//此时会报错!!
编译器认为p4=p3非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。
另外unique_ptr还有更聪明的地方:当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr将存在一段时间,编译器将禁止这么做
unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1; // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You")); // #2 allowed
如果确实想执行类似与#1的操作,要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。例如:
unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;
unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。
https://blog.csdn.net/zhizhengguan/article/details/122621885
添加了#pragma pack(n)后规则就变成了下面这样:
平台原因(移植原因)
性能原因:
重载 “==” 操作符
struct foo {
int a;
int b;
bool operator==(const foo& rhs) // 操作运算符重载
{
return( a == rhs.a) && (b == rhs.b);
}
};
元素的话,一个个比;
指针直接比较,如果保存的是同一个实例地址,则(p1==p2)为真;
是否可以使用memcmp来比较两个结构体是否相等呢?
memcmp函数是逐个字节进行比较的,而struct存在字节对齐,字节对齐时补的字节内容是随机的,会产生垃圾值,所以无法比较。https://blog.csdn.net/luolaihua2018/article/details/114338677
==来判断,会出错!#define MAX(X,Y) ((X)>(Y)?(X):(Y))
#undef取消某个符号的定义,进行重定义;函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化必须由程序员在程序中显式地指定。即函数模板允许隐式调用和显式调用而类模板只能显示调用。在使用时类模板必须加<T>,而函数模板不必。
template<…>处理的任何东西都意味着编译器在当时不为它分配存储空间,它一直处于等待状态直到被一个模板实例告知。在编译器和连接器的某一处,有一机制能去掉指定模板的多重定义。所以为了容易使用,几乎总是在头文件中放置全部的模板声明和定义。cout<<是一个函数,cout<<后可以跟不同的类型是因为cout<<已存在针对各种类型数据的重载,所以会自动识别数据的类型。printf没有类型检查,不安全。cout是通过运算符重载实现的,安全。printf是函数。cout是ostream对象,和<<配合使用。cout是有缓冲输出,输出过程会首先将输出字符放入缓冲区,然后输出到屏幕。 printf是无缓冲输出,有输出时立即输出。this绑定到左侧运算符对象。成员运算符函数的参数数量比运算符对象的数量少一个;至少含有一个类类型的参数;sizeof 运算符, :: 作用域运算符, ?:条件运算符, . 直接成员运算符, .*,->*:成员指针访问运算符如果是指变量的声明和定义
从编译原理上来说,声明是仅仅告诉编译器,有个某类型的变量会被使用,但是编译器并不会为它分配任何内存。而定义就是分配了内存。
如果是指函数的声明和定义
声明:一般在头文件里,对编译器说:这里我有一个函数叫function() 让编译器知道这个函数的存在。
定义:一般在源文件里,具体就是函数的实现过程写明函数体。
C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。
C++类型转换主要分为两种:隐式类型转换、显式类型转换(强制类型转换)。
所谓隐式类型转换,是指不需要用户干预,编译器默认进行的类型转换行为。C++中提供了explicit关键字,在构造函数声明的时候加上explicit关键字,能够禁止隐式转换。
强制类型转换操作符有四种:static_cast、const_cast、reinterpret_cast、dynamic_cast
所谓的静态,即在编译期内即可决定其类型的转换,用的也是最多的一种。它主要有如下几种用法:
int val_int;
double val_double = 3.1415;
val_int = static<int>(val_double);
专⻔⽤于 const 属性的转换,是四个转换符中唯⼀⼀个可以操作常量的转换符。
const_cast运算符用来去掉类型的const或volatile属性。但需要特别注意的是const_cast不是用于去除变量的常量性,而是去除指向常数对象的指针或引用的常量性,其去除常量性的对象必须为指针或引用。
对于将常量对象转换成非常量对象的行为,我们一般称其为“去掉const性质(cast away the const)”。一旦我们去掉了某个对象的const性质,编译器就不再阻止我们对该对象进行写操作了。
const char* ptr_char;
char* p = const_cast<char*>(ptr_char); // 正确:但是通过p写值的行为是未定义的行为
null(转型对象为指针时)或抛出异常bad_cast(转型对象为引用时)。pa.Author::eat()调用属于Author的拷贝。前置返回一个引用,后置返回一个对象
// ++i实现代码为:
int& operator++()
{
*this += 1;
return *this;
}
前置不会产生临时对象,后置必须产生临时对象,临时对象会导致效率降低
//i++实现代码为:
int operator++(int)
{
int temp = *this;
++*this;
return temp;
}
try
{
可能抛出异常的语句;(检查)
}
catch(类型名[形参名])//捕获特定类型的异常
{
//处理1;
}
catch(类型名[形参名])//捕获特定类型的异常
{
//处理2;
}
catch(…)//捕获所有类型的异常
{
}
delete this时,类对象的内存空间被释放。在delete this之后进行的其他任何函数调用,只要不涉及到this指针的内容,都能够正常运行。一旦涉及到this指针,如操作数据成员,调用虚函数等,就会出现不可预期的问题。会导致堆栈溢出。原因很简单,delete的本质是“为将被释放的内存调用一个或多个析构函数,然后释放内存”。显然,delete this会去调用本对象的析构函数,而析构函数中又调用delete this,形成无限递归,造成堆栈溢出,系统崩溃。
Union中的默认访问权限是public。union类型是共享内存的,以size最大的结构作为自己的大小。每个数据成员在内存中的起始地址是相同的。静态联编和动态联编;静态联编是指联编工作在编译阶段完成的,这种联编过程是在程序运行之前完成的,又称为早期联编。要实现静态联编,在编译阶段就必须确定程序中的操作调用(如函数调用)与执行该操作代码间的关系,确定这种关系称为束定,在编译时的束定称为静态束定。静态联编对函数的选择是基于指向对象的指针或者引用的类型。其优点是效率高,但灵活性差。动态联编是指联编在程序运行时动态地进行,根据当时的情况来确定调用哪个同名函数,实际上是在运行时虚函数的实现。这种联编又称为晚期联编,或动态束定。动态联编对成员函数的选择是基于对象的类型,针对不同的对象类型将做出不同的编译结果。C++中一般情况下的联编是静态联编,但是当涉及到多态性和虚函数时应该使用动态联编。动态联编的优点是灵活性强,但效率低。动态联编规定,只能通过指向基类的指针或基类对象的引用来调用虚函数,其格式为:指向基类的指针变量名->虚函数名(实参表)或基类对象的引用名.虚函数名(实参表)静态编译,编译器在编译可执行文件时,把需要用到的对应动态链接库中的部分提取出来,连接到可执行文件中去,使可执行文件在运行时不需要依赖于动态链接库;动态编译的可执行文件需要附带一个动态链接库,在执行时,需要调用其对应动态链接库的命令。所以其优点一方面是缩小了执行文件本身的体积,另一方面是加快了编译速度,节省了系统资源。缺点是哪怕是很简单的程序,只用到了链接库的一两条命令,也需要附带一个相对庞大的链接库;二是如果其他计算机上没有安装对应的运行库,则用动态编译的可执行文件就不能运行。算术
x = x + y;
y = x - y;
x = x - y;
异或
x = x^y;// 只能对int,char..
y = x^y;
x = x^y;
x ^= y ^= x;
https://blog.csdn.net/weixin_38169413/article/details/88380877
\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。strcpy函数源码(C语言)
char * strcpy(char * dest, const char * src) // 实现src到dest的复制
{
if ((src == NULL) || (dest == NULL)) //判断参数src和dest的有效性
{
return NULL;
}
char *strdest = dest; //保存目标字符串的首地址
while ((*strDest++ = *strSrc++)!='\0'); //把src字符串的内容复制到dest下
return strdest;
}
memcpy函数源码(C语言)
void *memcpy(void *memTo, const void *memFrom, size_t size)
{
if((memTo == NULL) || (memFrom == NULL)) //memTo和memFrom必须有效
return NULL;
char *tempFrom = (char *)memFrom; //保存memFrom首地址
char *tempTo = (char *)memTo; //保存memTo首地址
while(size -- > 0) //循环size次,复制memFrom的值到memTo中
*tempTo++ = *tempFrom++ ;
return memTo;
}
https://blog.csdn.net/qq_42697271/article/details/120957455
volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
声明时语法:int volatile vInt; 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。
可以,用const和volatile同时修饰变量,表示这个变量在程序内部是只读的,不能改变的,只在程序外部条件变化下改变,并且编译器不会优化这个变量。每次使用这个变量时,都要小心地去内存读取这个变量的值,而不是去寄存器读取它的备份。
注意:在此一定要注意const的意思,const只是不允许程序中的代码改变某一变量,其在编译期发挥作用,它并没有实际地禁止某段内存的读写特性。
i++、i+=这类的操作在多线程下都是不能保证变量的原子性的。string转const char*
string s = “abc”;
const char* c_s = s.c_str();
const char* 转string,直接赋值即可
const char* c_s = “abc”;
string s(c_s);
string 转char*
string s = “abc”;
char* c;
const int len = s.length();
c = new char[len+1];
strcpy(c,s.c_str());
char* 转string
char* c = “abc”;
string s(c);
const char* 转char*
const char* cpc = “abc”;
char* pc = new char[strlen(cpc)+1];
strcpy(pc,cpc);
char* 转const char*,直接赋值即可
char* pc = “abc”;
const char* cpc = pc;
https://blog.csdn.net/luke_sanjayzzzhong/article/details/100739797
拷贝构造函数的作用就是用来复制对象的,在使用这个对象的实例来初始化这个对象的一个新的实例。
class Base
{
public:
Base(){}
Base(const Base &b){..}
//
}
拷贝构造函数用来初始化一个非引用类类型对象,如果用传值的方式进行传参数,那么构造实参需要调用拷贝构造函数,而拷贝构造函数需要传递实参,所以会一直递归,导致内存溢出。。
class A{}; sizeof(A) = 1; //空类在实例化时得到⼀个独⼀⽆⼆的地址,所以为 1.
class A{virtual Fun(){} }; sizeof(A) = 4(32bit)/8(64bit) //当 C++ 类中有虚函数的时候,会有⼀个指向虚函数表的指针(vptr)
class A{static int a; }; sizeof(A) = 1;
class A{int a; }; sizeof(A) = 4;
class A{static int a; int b; }; sizeof(A) = 4;
https://blog.csdn.net/weixin_43219708/article/details/104165585
class Test
{
public:
Test() :_a(0)
{
t_count++;
}
Test(Test& a)
{
t_count++;
}
~Test()
{
t_count--;
}
static int GetCount()//静态成员函数
{
return t_count;
}
private:
int _a;
static int t_count;//静态成员变量 声明
};
int Test::t_count = 0;//静态成员变量在类外初始化,且不添加static关键字
void testCount()
{
Test t1, t2;
cout << Test::GetCount() << endl;
//cout << t_count << endl;
}
int main()
{
Test::GetCount();
Test t1, t2;
cout << Test::GetCount() << endl;//2
//cout << t1.GetCount() << endl;//也可以通过对象访问
testCount();//4
cout << Test::GetCount() << endl;//2
system("pause");
return 0;
}
sizeof的返回值=字符个数*字符所占的字节数,字符实际长度小于定义的长度,此时字符个数就等于定义的长度。若未给出定义的大小,分类讨论,对于字符串数组,字符大 小等于实际的字符个数+1;对于整型数组,字符个数为实际的字符个数。字符串每个字符占1个字节,整型数据每个字符占的字节数需根据系统的位数类确定,32位占4个字节。#include<iostream>
using namespace std;
template<typename type1,typename type2>//函数模板
type1 Max(type1 a,type2 b)
{
return a > b ? a : b;
}
void main()
{
cout<<"Max = "<<Max(5.5,'a')<<endl;
}
https://blog.csdn.net/qq_38836770/article/details/108871608
#include <iostream>
//第⼀种:gcc扩展,标记这个函数应当在main函数之前执⾏。同样有⼀个__attribute((destructor)),标记函数应当在程序结束之前(main结束之后,或者调⽤了exit后)执⾏;
__attribute((constructor))
void before() {
printf("before main 1\n");
}
//第⼆种:全局 static 变量的初始化在程序初始阶段,先于 main 函数的执⾏
int test1(){
printf("before main 2\n");
return 1;
}
static int i = test1();
// 第三种:利⽤ lambda 表达式
int a = []() {
printf("before main 3\n");
return 0;
}();
int main()
{
printf("main function\n");
return 0;
}
输出:
before main 1
before main 2
before main 3
main function
函数原型
char* strcpy(char* strDest, const char* strSrc)
char* strncpy(char* strDest, const char* strSrc, int pos)
strcpy函数: 如果参数 dest 所指的内存空间不够大,可能会造成缓冲溢出(buffer Overflow)的错误情况,在编写程序时请特别留意,或者用strncpy()来取代。
strncpy函数:用来复制源字符串的前n个字符,src 和 dest 所指的内存区域不能重叠,且 dest 必须有足够的空间放置n个字符。
memset(this, 0, sizeof *this);,将整个对象的内存全部置为0。对于这种情形可以很好的工作,但是下面几种情形是不可以这么使用的:
#include<time.h>
srand((unsigned)time(NULL));
cout<<(rand()%(b-a))+a;
https://zhuanlan.zhihu.com/p/213853588
在 C++11 之后,vector 容器中添加了新的方法:emplace_back() ,和 push_back() 一样都是在容器末尾添加一个新的元素进去。
emplace_back() 函数在原理上比 push_back() 有了一定的改进,包括在内存优化方面和运行效率方面。内存优化主要体现在使用了就地构造(直接在容器内构造对象,不用拷贝一个复制品再使用)+强制类型转换的方法来实现,在运行效率方面,由于省去了拷贝构造过程,因此也有一定的提升。
emplace_back() 和 push_back() 的区别,就在于底层实现的机制不同。push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);而 emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。
it=vec.erase(it);vec.clear():清空内容,但是不释放内存。vector().swap(vec):清空内容,且释放内存,想得到一个全新的vector。vec.shrink_to_fit():请求容器降低其capacity和size匹配。vec.clear();vec.shrink_to_fit();:清空内容,且释放内存。map 、set、multiset、multimap的底层实现都是红 黑 树。
红 黑 树 的 特 性 :
对于关联式容器来说,不需要做内存的拷贝和内存的移动,他们的存储方式是以节点的方式存储,只需要保留对父子节点的指针,所以实现插入和删除的时候只需要移动节点的指针。
不会失效,map和set的节点是malloc申请出来的,尽管insert了新的节点,内存并没有变,变得只是指针的指向。所以insert的时候迭代器并不会失效。但是对于序列式容器来说,当插入一个新的元素,有可能需要扩容,因为需要保证内存的连续存放,所以会引起迭代器的失效。
RB-TREE用二分查找法,时间复杂度为logn,所以从10000增到20000时,查找次数从log10000=14次到log20000=15次,多了1次而已。
logn,是比较高效的插入操作:
删除操作:
类的对象被创建时,编译系统为对象分配内存空间,并⾃动调⽤构造函数,由构造函数完成成员的初始化⼯作。即构造函数的作⽤:初始化对象的数据成员。
explict 的,来阻⽌⼀些隐式转换的发⽣。new int[] 是创建一个int型数组,数组大小是在[]中指定,例如:
int * p = new int[3]; //申请一个动态整型数组,数组的长度为[]中的值
没有赋初值,并定义一个整型指针p指向该地址空间开始处
new int()是创建一个int型数,并且用()括号中的数据进行初始化,例如:
int *p = new int(10); // p指向一个值为10的int数。
申请一个整型变量空间,赋初值为10,并定义一个整型指针p指向该地址空间
//把 src 所指向的字符串复制到 dest,注意: dest定义的空间应该⽐src⼤。
char* strcpy(char *dest,const char *src) {
char *ret = dest;
assert(dest!=NULL);//优化点1:检查输⼊参数
assert(src!=NULL);
while(*src!='\0')
*(dest++)=*(src++);
*dest='\0';//优化点2:⼿动地将最后的'\0'补上
return ret;
}
//考虑内存重叠的字符串拷⻉函数 优化的点
char* strcpy(char *dest,char *src) {
char *ret = dest;
assert(dest!=NULL);
assert(src!=NULL);
memmove(dest,src,strlen(src)+1);
return ret;
}
//把 src 所指向的字符串追加到 dest 所指向的字符串的结尾。
char* strcat(char *dest,const char *src) {
//1. 将⽬的字符串的起始位置先保存,最后要返回它的头指针
//2. 先找到dest的结束位置,再把src拷⻉到dest中,记得在最后要加上'\0'
char *ret = dest;
assert(dest!=NULL);
assert(src!=NULL);
while(*dest!='\0')
dest++;
while(*src!='\0')
*(dest++)=*(src++);
*dest='\0';
return ret;
}
//把 str1 所指向的字符串和 str2 所指向的字符串进⾏⽐较。
//该函数返回值如下:
//如果返回值 < 0,则表示 str1 ⼩于 str2。
//如果返回值 > 0,则表示 str1 ⼤于 str2。
//如果返回值 = 0,则表示 str1 等于 str2。
int strcmp(const char *s1,const char *s2) {
assert(s1!=NULL);
assert(s2!=NULL);
while(*s1!='\0' && *s2!='\0') {
if(*s1>*s2)
return 1;
else if(*s1<*s2)
return -1;
else {
s1++,s2++;
}
}
//当有⼀个字符串已经⾛到结尾
if(*s1>*s2)
return 1;
else if(*s1<*s2)
return -1;
else
return 0;
}
//在字符串 str1 中查找第⼀次出现字符串 str2 的位置,不包含终⽌符 '\0'。
char* strstr(char *str1,char *str2) {
char* s = str1;
assert(str1!='\0');
assert(str2!='\0');
if(*str2=='\0')
return NULL;//若str2为空,则直接返回空
while(*s!='\0') {//若不为空,则进⾏查询
char* s1 = s;
char* s2 = str2;
while(*s1!='\0'&&*s2!='\0' && *s1==*s2)
s1++,s2++;
if(*s2=='\0')
return str2;//若s2先结束
if(*s2!='\0' && *s1=='\0')
return NULL;//若s1先结束⽽s2还没结束,则返回空
s++;
}
return NULL;
}
//模拟实现memcpy函数 从存储区 str2 复制 n 个字符到存储区 dst。
void* memcpy(void* dest, void* src, size_t num) {
void* ret = dest ;
size_t i = 0 ;
assert(dest != NULL ) ;
assert(src != NULL) ;
for(i = 0; i<num; i++) {
//因为void* 不能直接解引⽤,所以需要强转成char*再解引⽤
//此处的void*实现了泛型编程
*(char*) dest = *(char*) src ;
dest = (char*)dest + 1 ;
src = (char*) src + 1 ;
}
return ret ;
}
//考虑内存重叠的memcpy函数 优化的点
void* memmove(void* dest, void* src, size_t num) {
char* p1 = (char*)dest;
char* p2 = (char*)src;
if(p1<p2) {//p1低地址p2⾼地址
for(size_t i=0; i!=num; ++i)
*(p1++) = *(p2++);
}
else {
//从后往前赋值
p1+=num-1;
p2+=num-1;
for(size_t i=0; i!=num; ++i)
*(p1--) = *(p2--);
}
return dest;
}
直接读取存放在内存中的⼗六进制数值,取低位进⾏值判断
int a = 0x12345678;
int *c = &a;
c[0] == 0x12 ⼤端模式
c[0] == 0x78 ⼩段模式
⽤共同体来进⾏判断
#include <stdio.h>
int main() {
union {
int a; //4 bytes
char b; //1 byte
} data;
data.a = 1; //占4 bytes,⼗六进制可表示为 0x 00 00 00 01
//b因为是char型只占1Byte, a因为是int型占4Byte
//所以,在联合体data所占内存中, b所占内存等于a所占内存的低地址部分
if(1 == data.b) {
//⾛到这⾥意味着说明a的低字节,被取给到了b
//即a的低字节存在了联合体所占内存的(起始)低地址,符合⼩端模式特征
printf("Little_Endian\n");
} else {
printf("Big_Endian\n");
}
return 0;
}
给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru
我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚
我将应用程序升级到Rails4,一切正常。我可以登录并转到我的编辑页面。也更新了观点。使用标准View时,用户会更新。但是当我添加例如字段:name时,它不会在表单中更新。使用devise3.1.1和gem'protected_attributes'我需要在设备或数据库上运行某种更新命令吗?我也搜索过这个地方,找到了许多不同的解决方案,但没有一个会更新我的用户字段。我没有添加任何自定义字段。 最佳答案 如果您想允许额外的参数,您可以在ApplicationController中使用beforefilter,因为Rails4将参数
几个月前,我读了一篇关于rubygem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:
?博客主页:https://xiaoy.blog.csdn.net?本文由呆呆敲代码的小Y原创,首发于CSDN??学习专栏推荐:Unity系统学习专栏?游戏制作专栏推荐:游戏制作?Unity实战100例专栏推荐:Unity实战100例教程?欢迎点赞?收藏⭐留言?如有错误敬请指正!?未来很长,值得我们全力奔赴更美好的生活✨------------------❤️分割线❤️-------------------------
HashMap中为什么引入红黑树,而不是AVL树呢1.概述开始学习这个知识点之前我们需要知道,在JDK1.8以及之前,针对HashMap有什么不同。JDK1.7的时候,HashMap的底层实现是数组+链表JDK1.8的时候,HashMap的底层实现是数组+链表+红黑树我们要思考一个问题,为什么要从链表转为红黑树呢。首先先让我们了解下链表有什么不好???2.链表上述的截图其实就是链表的结构,我们来看下链表的增删改查的时间复杂度增:因为链表不是线性结构,所以每次添加的时候,只需要移动一个节点,所以可以理解为复杂度是N(1)删:算法时间复杂度跟增保持一致查:既然是非线性结构,所以查询某一个节点的时候
嗨~大家好,这里是可莉!今天给大家带来的是7个C语言的经典基础代码~那一起往下看下去把【程序一】打印100到200之间的素数#includeintmain(){ inti; for(i=100;i 【程序二】输出乘法口诀表#includeintmain(){inti;for(i=1;i 【程序三】判断1000年---2000年之间的闰年#includeintmain(){intyear;for(year=1000;year 【程序四】给定两个整形变量的值,将两个值的内容进行交换。这里提供两种方法来进行交换,第一种为创建临时变量来进行交换,第二种是不创建临时变量而直接进行交换。1.创建临时变量来
1.postman介绍Postman一款非常流行的API调试工具。其实,开发人员用的更多。因为测试人员做接口测试会有更多选择,例如Jmeter、soapUI等。不过,对于开发过程中去调试接口,Postman确实足够的简单方便,而且功能强大。2.下载安装官网地址:https://www.postman.com/下载完成后双击安装吧,安装过程极其简单,无需任何操作3.使用教程这里以百度为例,工具使用简单,填写URL地址即可发送请求,在下方查看响应结果和响应状态码常用方法都有支持请求方法:getpostputdeleteGet、Post、Put与Delete的作用get:请求方法一般是用于数据查询,
Ⅰ软件测试基础一、软件测试基础理论1、软件测试的必要性所有的产品或者服务上线都需要测试2、测试的发展过程3、什么是软件测试找bug,发现缺陷4、测试的定义使用人工或自动的手段来运行或者测试某个系统的过程。目的在于检测它是否满足规定的需求。弄清预期结果和实际结果的差别。5、测试的目的以最小的人力、物力和时间找出软件中潜在的错误和缺陷6、测试的原则28原则:20%的主要功能要重点测(eg:支付宝的支付功能,其他功能都是次要的)80%的错误存在于20%的代码中7、测试标准8、测试的基本要求功能测试性能测试安全性测试兼容性测试易用性测试外观界面测试可靠性测试二、质量模型衡量一个优秀软件的维度①功能性功
前置步骤我们都操作完了,这篇开始介绍jenkins的集成。话不多说,看操作1、登录进入jenkins后会让你选择安装插件,选择第一个默认的就行。安装完成后设置账号密码,重新登录。2、配置JDK和Git都需要执行路径,所以需要先把执行路径找到,先进入服务器的docker容器,2.1JDK的路径root@69eef9ee86cf:/usr/bin#echo$JAVA_HOME/usr/local/openjdk-82.2Git的路径root@69eef9ee86cf:/#whichgit/usr/bin/git3、先配置JDK和Git。点击:ManageJenkins>>GlobalToolCon