1. 构造函数
struct CTest
{
int x;
int y;
};
struct CPPTest
{
int x;
int y;
};
void mainTest()
{
// 在初始化时,C语言的结构体,会创建x,y,但是不知道指向哪里,值为原来内存的值
CTest test;
// C++的结构体,在初始化时,会调用默认的构造函数,但默认的构造函数什么都不做,因此和C语言的结果一致,不知道指向哪里。
CPPTest cpptest;
std::cout << test.x << std::endl;
std::cout << test.y << std::endl;
}
只有使用构造函数才不会出现上面的问题。
struct CPPTest
{
public:
CPPTest(int x_, int y_) : x(x_), y(y_) {}
int x;
int y;
};
void mainTest()
{
CPPTest test(1, 2);
std::cout << test.x << std::endl;
std::cout << test.y << std::endl;
}
构造函数的类型
- 普通构造函数
- 复制构造函数:分为浅拷贝、深拷贝
- 移动构造函数:在第12节详细讲解
- 默认构造函数:当类没有任何构造函数时,编译器会生成一个默认构造函数,在最普通的类中,默认构造函数什么都没做。
class CPPTest
{
public:
// 普通构造函数
CPPTest(int x_, int y_) : x(x_), y(y_) {}
/* 复制构造函数(拷贝构造函数)
* 用另一个对象来初始对象对应的内存
* 形参一定是引用,不然会死循环,因为如果不是引用会一直调用复制构造函数
* 复制构造函数只在类定义时被调用
**/
CPPTest(const CPPTest& cppTest) : x(cppTest.x), y(cppTest.y) {}
// 默认构造函数,什么都不做,当有其它构造函数存在时,默认构造函数不会生成
CPPTest() {}
private:
int x;
int y;
}
注意:构造函数是C++提供的必须有的在对象创建时初始化对象的方法。
- 浅拷贝:进行简单的赋值拷贝,默认复制构造函数使用的方法。
- 深拷贝:在堆区重新开辟空间,进行深拷贝,主要避免内存的重复释放。
/*
* 对于有指针类型的成语变量时,此处会使用默认复制构造函数
* 会导致浅拷贝问题:即A.x与B.x值一样,指向同一片内存,调用析构时,会导致内存的重复释放
**/
class Test
{
public:
~Test()
{
if (x != nullptr) // 释放堆内存
{
delete x;
}
}
private:
unsigned* x;
unsigned y;
};
/*
* 使用深拷贝对浅拷贝问题进行解决
**/
class Test
{
public:
Test(const Test& test)
{
y = test.y;
x = new unsigned(*test.x); // 重新申请新的空间,进行深拷贝,解决内存的重复释放
}
~Test()
{
if (x != nullptr)
{
delete x;
}
}
private:
unsigned* x;
unsigned y;
};
2. 析构函数
当类对象被销毁时,就会调用析构函数。栈上对象销毁时就是函数结束销毁时,堆上对象销毁时就是堆内存被手动释放时。
class CPPTest
{
public:
~CPPTest() // 析构函数,如果自己不写,系统会默认有一个析构函数,不过什么都不做
{
std::cout << "CPPTest析构函数调用";
}
private:
int x;
int y;
};
3. this关键字
编译器将this关键字解释为指向函数所作用的对象的指针,我们可以通过C++到C语言的转换,来理解this关键字。
class CPPTest
{
public:
CPPTest(int x_, int y_) : x(x_), y(y_) {}
~CPPTest()
{
std::cout << "CPPTest析构函数调用" << std::endl;
}
void outPut()
{
std::cout << "CPPTest输出函数" << std::endl;
}
private:
int x;
int y;
};
将上述C++类代码转化为C会发现,以outPut()函数为例。
struct CTest
{
int x;
int y;
};
void outPut(struct CTest* this) // 会自动添加指针this,这就是this的本质
{
std::cout << "CPPTest输出函数" << std::endl;
};
void main()
{
CTest ctest(1, 2);
outPut(&ctest);
}
在C++中,并没有实际存在this的指针,而this只是被编译器解释的关键字,比如outPut()函数的形参列表中不存在this指针。
4. 常成员函数和常对象
常成员函数:无法修改成员变量的函数,可以理解为this指针指向的对象用const修饰的函数。
class CPPTest
{
public:
CPPTest(int x_, int y_) : x(x_), y(y_) {}
void outPut() // 类中修改成员变量
{
this->x = 10;
}
int x;
int y;
};
void outPut(CPPTest* const cppTest) // 类外修改成员变量
{
cppTest->x = 5;
}
若不允许修改类的成员变量,则按如下书写。
class CPPTest
{
public:
CPPTest(int x_, int y_) : x(x_), y(y_) {}
void outPut() const // 类中,直接在后面加const,不允许修改成员变量
{
this->x = 10; // 报错
}
int x;
int y;
};
void outPut(const CPPTest* const cppTest) // 类外函数加const,不允许修改类的成员变量
{
cppTest->x = 5; // 报错,此为const类
}
常对象:用const修饰的对象,定义好后就再也不需要改变成员变量的值了。
void mainTest()
{
const CPPTest test(1, 2); // 常对象,不能修改成员变量的值
}
注意:
(1)常成员函数无法调用普通成员函数。
(2)成员函数能写成常成员函数尽量写成常成员函数,减少出错几率。
(3)同名的常成员函数和普通成员函数会重载,常对象会调用常成员函数,普通对象会调用普通成员函数。
(4)常对象不能调用普通成员函数。
class CPPTest
{
public:
CPPTest(int x_, int y_) : x(x_), y(y_) {}
void outPut()
{
std::cout << "普通成员函数" << std::endl;
}
void outPut() const
{
std::cout << "常成员函数" << std::endl;
test(); // 报错,常成员函数无法调用普通成员函数
}
void test() {}
private:
int x;
int y;
};
void mainTest()
{
const CPPTest test(1, 2);
test.outPut(); // 常对象调用常成员函数
test.test(); // 报错,常对象不能调用普通成员函数
CPPTest test2(1, 2);
test2.outPut(); // 普通对象调用普通成员函数
}
5. 一些简单的关键字
- inline关键字
(1)普通函数在调用时需要给函数分配栈空间以供函数执行,压栈等操作会影响成员运行效率,因此C++提供了inline关键字,内联函数将函数体放到需要用函数的地方,以空间换时间。
(2)inline的使用,只需要在函数返回类型前加上inline关键字,就可以把函数指定为内联函数。注意inline需要放在函数定义前,放在函数声明处无效。
(3)inline关键字只是一个建议,若内联函数的代码很多,编译器可能不会采纳inline的意见。
class Test
{
public:
void outPut();
};
inline void Test::outPut() // inline关键字需放在函数定义前
{
XXX很多很多代码XXX; // 因为inline,这些代码会直接搬到下面,不会为函数创建一个栈
}
void mainTest()
{
Test test;
test.outPut(); // 运行时,XXX很多很多代码XXX将会搬到这里
}
- mutable关键字
(1)被mutable修饰的成员变量,永远处于可变的状态,即使处在常函数中,该变量也可以被更改。
(2)mutable现在主要用于统计函数调用次数。
(3)mutable不能修饰静态成员变量和常成员变量
class Test
{
public:
void outPut() const;
mutable unsigned outPutFuncCall; // 使用mutable,统计const函数调用的次数
};
void Test::outPut() const
{
++outPutFuncCall;
}
- default关键字
class Test
{
public:
Test() = default; // 默认构造函数
Test(const Test& test) = default; // 默认复制构造函数
Test& operator=(const Test& test) = default; // 默认赋值运算符
~Test() = default; // 默认析构函数
// default关键字能让代码更加直观
};
- delete关键字
/*
* 当我们不希望默认的函数被生成时,我们可以加delete告诉编译器
* 还可以使用将此函数声明为私有函数或将函数只声明不定义两种方法
**/
class Test
{
public:
Test() = delete;
Test(const Test& test) = delete;
Test& operator=(const Test& test) = delete;
~Test() = delete; // 析构可以delete,但最好不要
};
6. 友元
class Test
{
friend class TestFriend; // 声明友元类,在任何位置写都可以
friend void outPut(const Test& test); // 声明友元函数
private:
unsigned x;
unsigned y;
};
class TestFriend
{
public:
void outPut(const Test& test)
{
std::cout << test.x << std::endl; // 要访问Test的私有变量,必须在Test中使用friend,声明友元类
}
};
void outPut(const Test& test) // 在类外,要访问类的私有成员变量
{
std::cout << test.x << std::endl;
}
7. 重载运算符
运算符重载的格式:[返回值] operator[运算符] (参数…) { … };
[返回值]:返回值取决于重载运算符的作用,如a+b返回T,a+=b改变a的值返回void。
(参数…):在类中,单目运算符一般无参数,因为类有默认指针this,默认指向右边。双目运算符,this一般默认指向左边,参数为右边。
class Test
{
friend std::ostream& operator<< (std::ostream& os, const Test& test); // 一般输入输出流的重载用friend书写
friend std::istream& operator>> (std::istream& is, Test& test);
public:
Test(unsigned x_, unsigned y_) : x(x_), y(y_) {}
Test& operator++ () // 重载 ++T
{
++x;
++y;
return *this;
}
Test operator++ (int) // 重载 T++
{
Test t(*this);
x+=1;
y+=1;
return *t; // 根据T++原理,返回临时变量
}
void operator-- () // 重载 --T
{
--x;
--y;
}
int operator[] (unsigned i) const // 重载 T[]
{
return ivec[i];
}
void operator() (const std::string& str) const // 重载 T()
{
std::cout << "这是重载的函数" << str << std::endl;
}
Test operator+ (const Test& test) // 重载 T+X
{
Test t(*this); // 因为重载+,不要求改变原有的值
t.x += test.x;
t.y += test.y;
return t;
}
Test& operator= (const Test& test) // 重载 T=X
{
if (this == &test)
return *this;
x = test.x;
y = test.y;
return *this;
}
bool operator> (const Test& test) // 重载 T>X
{
return (x > test.x) && (y > test.y);
}
std::vector<int> ivec{ 1, 2, 3, 4, 5, 6 };
private:
unsigned x;
unsigned y;
};
/*
* 输入输出流的重载
**/
std::ostream& operator<< (std::ostream& os, const Test& test)
{
os << test.x << ',' << test.y << std::endl;
return os; // 需要返回ostream的引用,使得<<继续进行
}
std::istream& operator>> (std::istream& is, Test& test)
{
is >> test.x >> test.y;
return is; // 需要返回istream的引用,使得>>继续进行
}
void mainTest()
{
Test test(1, 2);
++test; // 执行++T
--test; // 执行--T
test[3]; // 输出4
test("abcd"); // 输出“这是重载的函数abcd”
Test test2(2, 3);
test = test + test2;
test = test2;
test > test2; // 返回false
}
注意:赋值运算符重载和复制构造函数的区别,主要取决于是对象定义时还是对象定义后。
char* strTest = new char[10]();
Test* test = new Test(strTest);
Test test2(*test); // 调用复制构造函数
Test test3 = test2; // 对象定义时,调用复制构造函数
Test test4(strTest);
test4 = test2; // 对象定义后,调用赋值运算符重载
对于流运算符,因为是双目运算符,this本应该指向左边,但左操作数是一个流,会产生冲突,因此使用友元函数friend。如果不用友元,就会变成如下情况。
test << std::cout;
不能重载的运算符:三目运算符“?:”
8. 继承及原理
原理:
(1)由父类对象创建子类对象之前,子类创建会先调用父类的构造函数,初始化父类的部分,再初始化自己的部分,因此父类得到指针一定能指向子类,因为指针只会检查父类的成员变量是否存在,不会管子类的其余部分。
(2)子类构造时,会先调用父类的构造函数,在调用子类的构造函数,子类析构时,会先调用子类的析构函数,再调用父类的析构函数。
(3)父类没有默认构造时,子类又没调用,则无法编译。
// 父类
class FatherTest
{
public:
FatherTest(const std::string name_, const int num_) : name(name_), num(num_)
{
std::cout << "父类构造函数" << std::endl;
}
~FatherTest()
{
std::cout << "父类析构函数" << std::endl;
}
protected: // protected使子类能够访问
std::string name;
int num;
};
// 子类
class ChildTest : public FatherTest
{
public:
ChildTest(const std::string name_, const int num_, int i_) : FatherTest(name_, num_), i(i_)
{
std::cout << "子类构造函数" << std::endl; // 子类构造时会先调用父类的构造函数在调用子类的构造函数
}
// 从内存上讲,子类也会先生成父类的成员变量
~ChildTest()
{
std::cout << "子类析构函数" << std::endl;
}
private:
int i;
};
// mian()函数
void classTestMain()
{
FatherTest* p = new ChildTest("小明", 1, 1); // 父类指针能指向子类
delete(p);
/* 运行结果:(此处会造成子类内存泄漏的严重问题,下节虚函数会讲解处理方法)
* 父类构造函数
* 子类构造函数
* 父类析构函数
**/
ChildTest* p = new ChildTest("小明", 1, 1); // 父类指针能指向子类
delete(p);
/* 运行结果:
* 父类构造函数
* 子类构造函数
* 子类析构函数
* 父类析构函数
**/
}
9. 虚函数
9.1 virtual与override
当父类指针指向子类对象时,且子类重写父类某一函数,父类指针调用该函数,就会产生以下可能:
(1)该函数为虚函数,父类指针调用的是子类的成员函数。
(2)该函数不是虚函数,父类指针调用的是父类的成员函数。
注意:
(1)一定要在需要子类多态成员函数的父类中添加virtual关键字。
(2)子类和父类的虚函数除了virtual关键字,其它必须完全一样,为了防止不一样,加入了override关键字来检查报错。
(3)父类的析构函数必须为虚函数,当父类指针指向子类对象时,子类很容易内存泄漏。
// 父类
class FatherTest
{
public:
FatherTest(const std::string name_, const int num_) : name(name_), num(num_)
{
std::cout << "父类构造函数" << std::endl;
}
virtual ~FatherTest() // 父类的析构函数必须为虚函数,否则会内存泄漏
{
std::cout << "父类析构函数" << std::endl;
}
virtual void outPut() const // 一定要在父类中加上virtual,只在子类加virtual会出问题
{
std::cout << "父类outPut()执行" << std::endl;
}
protected:
std::string name;
int num;
};
// 子类
class ChildTest : public FatherTest
{
public:
ChildTest(const std::string name_, const int num_, int i_) : FatherTest(name_, num_), i(i_)
{
std::cout << "子类构造函数" << std::endl;
}
~ChildTest()
{
std::cout << "子类析构函数" << std::endl;
}
virtual void outPut() const override // override负责检测父类中有无完全一致的函数
{
std::cout << "子类outPut()执行" << std::endl;
}
private:
int i;
};
// 测试父类指针调用的是自己的还是子类的函数
void testFunc(const FatherTest* ptest)
{
ptest->outPut();
delete ptest; // new的对象,一定要delete
}
// mian()函数
void classTestMain()
{
testFunc(new ChildTest("小明", 1, 1));
}
/* 运行结果:
* 父类构造函数
* 子类构造函数
* 子类outPut()执行
* 子类析构函数
* 父类析构函数
**/
9.2 虚函数的原理
函数的静态绑定和动态绑定
静态绑定:程序在编译时就已经确定了函数的地址,比如非虚函数就是静态绑定。
动态绑定:程序在编译时确定的是程序寻找函数地址的方式,只有在程序运行时才可以真正确定程序的地址,比如虚函数。
void classTestMain()
{
FatherTest* ptest = new ChildTest("小明", 1, 1); // 动态绑定
FatherTest test("小明", 1); // 直接在栈上创建,静态绑定
delete(ptest);
}
虚函数实现多态的原理
每个类有且仅有一个虚函数表,所有的对象共用。每个有虚函数的类都有一个虚函数表,对象其实就是虚函数表的指针,编译时编译器只告诉了程序会在运行时查找虚函数表的对应函数。每个类都有自己的虚函数表,当父类指针引用的是子类虚函数表时,自然调用的是子类的函数。
9.3 纯虚函数
只要将一个虚函数写为纯虚函数,那么该类将被认为是无实际意义的类,无法产生对象,就算纯虚函数写了实现的部分,编译器也会自动忽略。
// 纯虚函数类
// 注意虽然其它函数无意义,但构造函数和析构函数不能省略,因为子类要继承
class FatherTest
{
public:
FatherTest(const std::string name_, const int num_) : name(name_), num(num_)
{
std::cout << "父类构造函数" << std::endl;
}
virtual ~FatherTest()
{
std::cout << "父类析构函数" << std::endl;
}
virtual void outPut() const = 0; // 纯虚函数,FatherTest无法产生对象
protected:
std::string name;
int num;
};
void classTestMain()
{
FatherTest* ptest = new ChildTest("小明", 1, 1); // 正确,并没有用纯虚函数类产生对象,生成的是子类的对象
FatherTest test("小明", 1); // 错误,纯虚函数类无法产生对象
delete(ptest);
}
10. 静态成员变量与静态函数
1.静态成员变量
(1)静态成员变量的生命周期为整个程序。
(2)类的静态成员变量创建在静态区,所以可以直接通过类名来调用。
(3)静态成员变量必须在类外进行初始化,否则会报错误,“无法解释的外部符号XXX” 即未定义。
(4)静态成员变量在编译期就要被创建,所以不能用构造函数进行初始化。
class Test
{
private:
static unsigned i;
};
unsigned Test::i = 0; // 类的静态成员变量必须要在类外初始化
2.静态成员函数
静态成员函数就是为静态成员变量服务的,因为静态成员变量可以用类名调用,而会破坏封装性。使用静态成员函数就是为了保护静态成员变量的封装性。
class Test
{
public:
static unsigned getStaticI() // 静态成员函数就是为了保护静态成员变量的封装性
{
return i;
}
private:
static unsigned i;
};
unsigned Test::i = 0;
11. RTTI
RTTI(Run Time Type Identification)即运行时类型识别。RTTI是C++判断指针或引用实际类型的唯一方式。
RTTI的使用场景:(1)异常处理;(2)IO操作
RTTI的使用方法:
(1)typeid函数
typeid函数返回一个叫做type_info的结构体,该结构体中包含对象的实际信息,其中name()函数返回函数的真实名称。而type_info的其他函数没什么用。
注意:当使用typeid函数时,父类和子类必须有虚函数,否则类型判断可能出错。
void classTestMain()
{
FatherTest* ptest = new ChildTest("小明", 1, 1);
std::cout << typeid(ptest).name() << std::endl; // 输出:class FatherTest *
std::cout << typeid(*ptest).name() << std::endl; // 输出:class ChildTest
delete(ptest);
}
(2)dynamic_cast函数
C++提供的将父类指针转化为子类指针的函数。
void classTestMain()
{
FatherTest* pFatherTest = new ChildTest("小明", 1, 1);
if (std::string(typeid(*pFatherTest).name()) == "class ChildTest")
{
ChildTest* pChildTest = dynamic_cast<ChildTest*>(pFatherTest); // 将父类指针转化为子类指针
if (pChildTest)
{
std::cout << "cast to ChildTest success" << std::endl;
}
}
}
12. 移动构造函数
对象移动
(1)对一个体积比较大的类进行大量的拷贝操作是非常消耗性能的,因此C++11加入“对象移动”。
(2)对象移动就是把该对象占据的内存空间的访问权限转移给另一个对象。
class Test
{
public:
Test(char* test_) : str(test_) {}
// 普通复制构造函数
Test(const Test& test)
{
if (test.str)
{
str = new char[strlen(test.str) + 1]();
strcpy_s(str, strlen(test.str) + 1, test.str);
}
else
{
str = nullptr;
}
}
// 移动构造函数
Test(Test&& test)
{
if (test.str)
{
str = test.str;
test, str = nullptr;
}
else
{
str = nullptr;
}
}
// 普通赋值运算符重载
Test& operator=(const Test& test)
{
if (this == &test)
{
return *this;
}
if (str)
{
delete[] str;
str = nullptr;
}
if (test.str)
{
str = new char[strlen(test.str) + 1];
strcpy_s(str, strlen(test.str) + 1, test.str);
}
else
{
str = nullptr;
}
return *this;
}
// 移动赋值运算符重载
Test& operator==(Test&& test)
{
if (this == &test)
{
return *this;
}
if (str)
{
delete[] str;
str = nullptr;
}
if (test.str)
{
str = test.str;
test.str = nullptr;
}
else
{
str = nullptr;
}
return *this;
}
private:
char* str = nullptr;
};
注意:移动构造函数的意义是对临时对象的拷贝(即马上要销毁的对象使用移动操作)。
Test makeTest()
{
Test t;
return t;
}
void classTestMain()
{
Test t = makeTest(); // 使用移动赋值运算符重载
}
生成默认移动函数和默认赋值运算符的条件:
(1)只有一个类没有定义任何自定义的拷贝构造和拷贝赋值运算符重载。
(2)类的每个非静态成员变量都可以移动(基础类型都可以移动、有移动语义的类)。
条件(1)和(2)同时满足,系统才会生成默认的移动操作函数。