写在前面本文是我在学习c++的过程中整理出来的一些自己比较模糊的知识点,主要记录自己的学习过程,方便今后复习查阅。本文将持续更新。
优先级由高到低:
.成员选择(对象)
->成员选择(指针)
++后置自增运算符
–后置自减运算符
从左到右结合
*取值运算符
&取地址运算符
++前置自增运算符
–前置自减运算符
从右到左结合
用来释放动态分配的内存,如malloc()。必须要有指针指向malloc()动态分配的内存,否则容易出现野指针
函数原型 void *memcpy(void * dst , void * src, size-t size)
函数作用:将src所指的内存单元拷贝到dst所指的内存单元,一共拷贝size个字节。
一些类主要用于存储和组织数据元素,因此类模板就是为了数据结构而诞生的,将泛型编程的思想应用于类,使得类的思想不关注数据类型的具体类型,而只关注类所需实现的功能。使用类模板的典型类如:数组类、链表类、Stack 类、Queue 类等。
template <模板参数表>//模板参数表通常用关键字 calss 或者 typename声明
class 类名
{类成员声明}
如果需要在类模板以外定义其成员函数,则需要采用以下的形式:
template<模板参数表>
返回值类型 类名<模板参数标识符列表>::函数名(参数表)
类模板示例:
1、在C语言中,NULL通常被定义为:#define NULL ((void *)0)。
所以说NULL实际上是一个空指针,如果在C语言中写入以下代码,编译是没有问题的,因为在C语言中把空指针赋给int和char指针的时候,发生了隐式类型转换,把void指针转换成了相应类型的指针。
int pi = NULL;
char pc = NULL;
2、C++程序中的NULL
因为C++是强类型语言,void是不能隐式转换成其他类型的指针的,以上代码编译器编译会报错,所以实际上编译器提供的头文件做了相应的处理:
#define NULL 0
3、 c++中nullptr
解决NULL代指空指针存在的二义性问题,在C++11版本(2011年发布)中特意引入了nullptr这一新的关键字来代指空指针,从上面的例子中我们可以看到,使用nullptr作为实参,确实选择了正确的以void作为形参的函数版本。
=delete用法:参考原文链接:https://blog.csdn.net/lmb1612977696/article/details/80035487
1. 禁止使用编译器默认生成的函数
编译器默认为一个类生成的默认函数有:
默认构造函数
默认析构函数
默认拷贝构造函数
默认赋值函数
移动构造函数
移动拷贝函数
假如上面的几个函数中,不想使用其中某个,可以将其定义为private,或者使用=delete。
2.delete 关键字可用于任何函数,不仅仅局限于类的成员函数
3. 模板特化:在模板特例化中,可以用delete来过滤一些特定的形参类型
例如:Widget 类中声明了一个模板函数,当进行模板特化时,要求禁止参数为 void* 的函数调用。按照私有不实现思路,应该是将特例化的函数声明为私有型。
=default的含义:
struct Sales_data{
//新增的构造函数
Sales_data() = default;
Sales_data(const std::string &s):bookNo(s){}
//一些数据成员
std::string bookNo;
unsigned units_sold = 0;
};
解释Sales_data() = default的含义:首先请明确一点,该函数不接受任何参数,所以它是一个默认构造函数,我们定义这个函数的意义仅仅是因为我们既需要这个形式的默认构造函数又需要其他形式的构造函数,在C++11新标准中,如果我们需要默认的行为,那么可以通过在参数列表后面写上=default来要求编译器生成构造函数。其中,=default既可以和声明一起出现在类内部,也可以和定义一起出现在类外部。和其他函数一样,如果=default在类的内部,则默认构造函数是内联的;如果它出现在类的外部,则该成员默认情况下不是内联的
1、对像生存周期结束时,对象被销毁
2、delete指向对象的指针时,或者delete对象的基类类型指针,并且基类的虚构函数被声明为虚函数时
3、对象i是对象o的成员,o的析构函数被调用时,对象i的析构函数也被调用。
浅拷贝
浅拷贝又叫值拷贝,本质上来说拷贝的目标对象和源对象用的是同一份实体,只是引用的变量名不同而已,访问的还是同一块内存空间。假设有一个String类,String s1;String s2(s1);在进行拷贝构造的时候将对象s1里的值全部拷贝到对象s2里。此时若类中没有重载拷贝构造函数,当进行对象赋值操作时编译器会调用默认拷贝构造函数,这就是浅拷贝。
//默认拷贝构造函数的原型
STRING( const STRING& s )
{
_str = s._str;
}
//默认赋值运算函数原型
STRING& operator=(const STRING& s)
{
if (this != &s)//不允许自己赋值给自己
{
this->_str = s._str;
}
return *this;
}
当声明一个类有指针成员函数时,或者构造函数中有使用new开辟的空间时,该类对象之间直接赋值会导致:
1、被赋值对象中的指针丢失原始指向的内存,造成内存泄漏问题。
2、对象被销毁时调用析构函数,两次都释放的是同一块内存,导致程序崩溃。
#include "stdafx.h"
#include
using namespace std;
class STRING {
public:
STRING(const char* s = "") :_str(new char[strlen(s) + 1])//构造函数中有new开辟的空间
{
strcpy_s(_str, strlen(s) + 1, s);
}
//拷贝构造函数原型
/*STRING(const STRING & s) =
{
this->_str = s._str;
}*/
//重载拷贝构造函数
STRING(const STRING & s)
{
_str = new char[strlen(s._str) + 1];
strcpy_s(_str, strlen(s._str) + 1, s._str);
}
//赋值运算符重载函数
STRING & operator=(const STRING &s)
{
if (this != &s)//不允许自己赋值给自己
{
delete[] _str;//1 销毁自己开辟的内存空间
this->_str = new char[strlen(s._str) + 1];//2 开辟新的内存空间,大小和源对象相同
strcpy_s(this->_str, strlen(s._str) + 1, s._str);//3 将源对象内存中的东西复制到新开辟的内存空间中
}
return *this;
}
//析构函数
~STRING()
{
cout << "~STRING" << endl;
if (_str)
{
delete[] _str;
_str = nullptr;
}
}
void show()
{
cout << this->_str << endl;
}
private:
char * _str;
};
int main()
{
STRING S1("IAM");
STRING S2;
S2.operator=(S1);
STRING S3(S1);//调用的是重载后的拷贝构造函数
STRING S4 = S1;//调用的是重载后的拷贝构造函数
STRING S5;
S5 = S1;//调用的是赋值运算符重载函数
S2.show();
system("pause");
return 0;
}
解决浅拷贝两次释放同一块内存问题需要使用深拷贝。
深拷贝:拷贝的时候先开辟出和源对象大小一样的空间,然后将源对象里的内容拷贝到目标对象中去,这样两个指针就指向了不同的内存位置。并且里面的内容是一样的,这样不但达到了我们想要的目的,还不会出现问题,两个指针先后去调用析构函数,分别释放自己所指向的位置。即为每次增加一个指针,便申请一块新的内存,并让这个指针指向新的内存,深拷贝情况下,不会出现重复释放同一块内存的错误。
实现深拷贝需要重载拷贝构造函数和赋值运算符重载函数。
重载拷贝构造函数的传统实现:1、开辟一个和源对象内存大小相同的空间 2、把源对象中的内容复制到新内存空间
赋值运算符重载函数的传统实现:1、释放目标对象用new开辟的内存空间 2、开辟与源对象大小相同的内存空间 3、把源对象的内容拷贝到目标对象中。
//重载拷贝构造函数
STRING(const STRING &s):_str(nullptr)
{
_str = new char[strlen(s._str) + 1];//开辟一个和源对象内存大小相同的空间
strcpy_s(_str , strlen(s._str) + 1 , s._str);//把源对象中的内容复制到新内存空间
}
//赋值运算符重载函数
STRING & operator=(const STRING &s)
{
if (this != &s)//不允许自己赋值给自己
{
delete[] _str;//1 销毁自己开辟的内存空间
this->_str = new char[strlen(s._str) + 1];//2 开辟新的内存空间,大小和源对象相同
strcpy_s(this->_str, strlen(s._str) + 1, s._str);//3 将源对象内存中的东西复制到新开辟的内存空间中
}
return *this;
}
重载拷贝构造函数和赋值运算符重载函数现代实现:1、调用构造函数完成tmp对象的构造和初始化(值拷贝) 2、使用swap函数交换tmp和目标拷贝对象所指向的内容
STRING( const STRING& s ):_str(NULL)
{
STRING tmp(s._str);// 调用了构造函数,完成了空间的开辟以及值的拷贝
swap(this->_str, tmp._str); //交换tmp和目标拷贝对象所指向的内容
}
STRING& operator=(const STRING& s)
{
if ( this != &s )//不让自己给自己赋值
{
STRING tmp(s._str);//调用构造函数完成空间的开辟以及赋值工作
swap(this->_str, tmp._str);//交换tmp和目标拷贝对象所指向的内容
}
return *this;
}
使用new初始化对象的指针成员时必须特别小心。具体地说,应当这样做。
1)如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete。
2)new和delete必须相互兼容。new对应于delete,new[]对应于delete[]。
3) 如果在创建对象的时候采用动态申请(new),那么需要显式的调用类的析构函数。
为了避免上述浅拷贝的情况需要重载赋值运算符。
1、如果检测到构造函数和赋值运算符重载函数同时存在,则会调用赋值运算符重载函数;
2、如果检测到的构造函数,就会调用这个构造函数。
综合上述9、10内容,我们可以知道针对以下情况,需要显式地提供赋值运算符重载函数(即自定义赋值运算符重载函数):
1、用非类A类型的值为类A的对象赋值时(当然,这种情况下我们可以不提供相应的赋值运算符重载函数,而只提供相应的构造函数)。
2、当用类A类型的值为类A的对象赋值,且类A的数据成员中含有指针的情况下,必须显式提供赋值运算符重载函数;
ClassA obj2 ;//对象obj2的声明及定义
obj2=obj1;//在对象obj2已经存在的情况下,用obj1来为obj2赋值,调用的是赋值运算符重载函数.当程序没有显式地提供一个以“本类或本类的引用”为参数的赋值运算符重载函数时,编译器会自动生成一个默认的赋值运算符重载函数,即调用的是构造函数。
ClassA obj2 = ob1;//是用obj1来初始化obj2,调用的是拷贝构造函数。
有两种传递方法,一种是function(int a[]); 另一种是function(int *a)
这两种两种方法在函数中对数组参数的修改都会影响到实参本身的值!
对于第一种,根据之前所学,形参是实参的一份拷贝,是局部变量。但是数组是个例外,因为数组的数据太多了,将其一一赋值既麻烦又浪费空间,所以数组作为参数传递给函数的只是数组首元素的地址,数据还是在内存里的,函数在需要用到后面元素时再按照这个地址和数组下标去内存查找。也就是说后面的元素根本没到函数里来。所以,这里也不能在test()函数内部用sizeof求数组的大小,必须在外面算好了再传进来。
对于第二种,则是传址调用,无需再说。
这里还有几点需要注意:
1.在以上两个函数的形参前面加上const则表示整个数组只读,而不是只有首地址对应存储的数据只读。
2.第二种形式不能用C++11中的for…auto来循环打印。
3.数组的大小要用sizeof()来求,不能用.size(),因为.size()只有struct 或者union才能用, vector算是struct!
4.如果在函数内部又声明一个int* tmp类型的变量,然后把p赋值给tmp, 通过tmp修改数数组也是一样,都会修改实参本身!
1、定义一个类模板
template
struct binaryTreeNode {
T element;
binaryTreeNode * leftChild,
* rightChild;
};
2、typedef
typedef int myType;
typedef strcut _binaryTreeNode{
myType element;
struct _binaryTreeNode * leftChild;
struct _binaryTreeNode * rightChild;
}binaryTreeNode;
3、
strcut _binaryTreeNode{
int element;
_binaryTreeNode * leftChild;
_binaryTreeNode * rightChild;
};
int *a = new int[5];
class A {…} //声明一个类 A
A *obj = new A(); //使用 new 创建对象
delete []a;
delete obj;
递归:递归问题可以分解成各个子问题,各个子问题的解决思路和原始问题一致,递归问题都有终止条件。
迭代:也称辗转法,是一种不断用变量的旧值递推新值的过程,跟迭代法相对应的是直接法(或者称为一次解法),即一次性解决问题。
迭代与普通循环的区别是:迭代时,循环代码中参与运算的变量同时是保存结果的变量,当前保存的结果作为下一次循环计算的初始值。
递归与普通循环的区别是:循环是有去无回,而递归则是有去有回(因为存在终止条件)。
出处:https://www.cnblogs.com/yuanch2019/p/11591995.html
reverse函数功能是逆序(或反转),多用于字符串、数组、容器。头文件是#include
reverse函数用于反转在[first,last)范围内的顺序(包括first指向的元素,不包括last指向的元素),reverse函数无返回值
eg.:
string str=“hello world , hi”;
reverse(str.begin(),str.end());//str结果为 ih , dlrow olleh
vector v = {5,4,3,2,1};
reverse(v.begin(),v.end());//容器v的值变为1,2,3,4,5
首先引入头文件:
获取变量类型的语句是:typeid(variable).name(),其中 “variable”是你定义的变量名称。