类#
C 语言的结构体
struct TEST
{ // NOTE: C语言的结构体不能有函数
// why: 因为一个结构体对象是要声明在栈或者堆中,而任何一个函数都要声明在代码区中
int i;
int t;
};C++类的本质就是 C 语言的结构体外加几个类外的函数,C++最后都要转化为 C 语言来实现,类外的函数就是通过 this 来指向这个类的
简单介绍#
面试的时候经常会听到一个问题,谈一下对面向对象和面向过程的理解
1. 面向对象和面向过程是一个相对的概念
2. 面向过程是按照计算机的工作逻辑来编码的方式,最典型的面向过程的语言就是 C 语言了,C 语言直接对应汇编,汇编又对应电路
3. 面向对象是按照人类的思维来编码的一种方式,C++完全支持面向对象功能,可以按照人类的思维来处理问题
4. 举个例子,要把大象装冰箱,按照人类的思路自然是分三步,打开冰箱,将大象装进去,关上冰箱。要实现这三步,我们就要首先有人,冰箱这两个对象。人有给冰箱发指令的能 力,冰箱有能够接受指令并打开或关闭门的能力。但是从计算机的角度讲,计算机只能定义一个叫做人和冰箱的结构体。人有手这个部位,冰箱有门这个部位。然后从天而降一个函数,是这个函数让手打开了冰箱,又是另一个函数让大象进去,再是另一个函数让冰箱门关上。那么,如何用面向过程的C语言模拟出面向对象的能力呢?类就诞生了,在类中可以定义专属于类的函数,让类有了自己的动作。回到那个例子,人的类有了让冰箱开门的能力,冰箱有了让人打 开的能力,不再需要天降神秘力量了。从开发者的角度讲,面向对象显然更利于程序设计。用面向过程的开发方式,程序一旦大了,各种从天而降的函数会非常繁琐,一些用纯 c 写的大型程序,实际上也是模拟了面向对象的方式。
#include <iostream>
// NOTE: 面向对象一大的特点就是能在类中定义函数
class IceChest {
public:
// NOTE: 冰箱开门或关门
void openDoor() {}
void closeDoor() {}
};
class Person {
public:
// NOTE: 人能打开或关闭冰箱
void openIceChest(const IceChest &iceChest) {}
};
int main() { return 0; }构造函数#
类相当于定义了一个新类型,该类型生成在堆或栈上的对象时内存排布和 C 语言相同。但是 C++规定,C++有在类对象创建时就在对应内存将数据初始化的能力,这就是构造函数
#include <iostream>
// 仿C写法
struct CTest { // NOTE: C语言是无法在定义对象是将里面的数据初始化的
int i;
int i2;
};
struct CPPTest {
public:
CPPTest(int i_, int i2_) : i(i_), i2(i2_) {}
//复制构造函数
CPPTest(const CPPTest& t2) :i(t2.i) , i2(t2.i2) {}
// NOTE: 加const是为了只读取成员变量的值,不能修改值
int i;
int i2;
};
int main() {
CTest t1;
CPPTest t2( 1,2 ); // NOTE: C++中如果未初始化对象,会调用构造函数,但是默认的构造函数是什么都不做的
std::cout << t1.i << std::endl << t1.i2 << std::endl;
std::cout << t2.i << std::endl << t2.i2 << std::endl;
return 0;
}构造函数的类型
普通构造函数自定义参数,一般是初始化成员变量
复制构造函数用另一个对象来初始化对象对应内存
移动构造函数也是用一个对象来初始化对象
默认构造函数当类中没有构造函数是,编译器会为该类生成一个默认的构造函数,在最普通的类中,默认构造函数什么都没做,对象对应的内存没有初始化
总结: 构造函数就是C++提供的 必须有的 在创建对象初始化对象的方法
默认什么都不做也是一种初始化方式
移动构造函数#
对象移动的概念#
- 对一个体积比较大的类进行大量的拷贝操作是非常消耗性能的,因此 C++11 中加入了
对象移动的操作 - 所谓的对象移动,其实就是把该对象所占据的内存空间的访问权限转移给另一个对象,比如一块内存原本属于
A,在进行“移动语义”后,这块内存就属于B了
移动语义为什么可以提高效率#
因为我们的各种操作会经常进行大量的“复制构造”,“赋值运算”操作,这两个操作非常耗时间。移动构造是直接转移权限,效率就提高了
注意事项:
- 在进行转移操作后,被转移的对象就不能继续使用了,所以对象移动一般都是对临时对象进行操作,因为临时对象就要被销毁了
#include <iostream>
#include <string.h>
#include <string>
class Test {
public:
//NOTE: 不需要重新new一遍来消耗时间
Test() = default;
// Test(const Test &test) {
//
// if (test.str) {
// str = new char[strlen(str) + 1](); // strlen在统计的时候,不会算结束符
// // strcpy()(str , strlen(test.str) + 1 , test.str); // WARNING: it doesn't
// // work
// strcpy(str, 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(str) + 1]();
// strcpy(str, test.str);
// } else {
// str = nullptr;
// }
// return *this;
// }
Test &operator=(Test &&test) { // NOTE:不要加const,后面要修改
//因为用右值引用函数参数就是为了让其绑定到一个右值上去的!就是说这个右值引用是一定要变的,
//但是一旦加了const就无法改变
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;
}
int main() {
Test t = makeTest();
return 0;
}默认移动构造函数和默认移动赋值运算符#
- 会默认生成移动构造函数和移动赋值运算符的条件:
- 只有一个类没有定义任何自己版本的拷贝操作(拷贝构造、拷贝赋值运算符),且类的每个非静态成员都可以移动,系统才会合成
- 可以移动的意思是可以就行移动构造、移动赋值。所有的基础类型都是可以移动的。有移动语义的类也是可以移动的
析构函数#
当类对象被销毁时,就会调用析构函数。栈上对象的销毁时机就是函数栈销毁时,堆上的对象销毁时机就是该堆内存被手动释放是,如果用 new 申请的这块堆内存,那调用 delete 销毁这块内存是就会调用析构函数
#include <iostream>
// 仿C写法
struct CTest { // NOTE: C语言是无法在定义对象是将里面的数据初始化的
int i;
int i2;
};
struct CPPTest {
public:
CPPTest(int i_, int i2_ , int i3) : i(i_), i2(i2_) , pi(new int(i3)) {}
//复制构造函数
CPPTest(const CPPTest& t2) :i(t2.i) , i2(t2.i2) , pi(new int (*t2.pi)) {}
// NOTE: 加const是为了只读取成员变量的值,不能修改值
//模拟默认构造函数
CPPTest(){}
//析构函数
~CPPTest() {
delete pi;
} // NOTE:因为析构函数不带参数,所以无法重载
int i;
int i2;
int* pi;
};
int main() {
CTest t1;
CPPTest t2( 1,2 , 3); // NOTE: C++中如果未初始化对象,会调用构造函数,但是默认的构造函数是什么都不做的
std::cout << t1.i << std::endl << t1.i2 << std::endl;
std::cout << t2.i << std::endl << t2.i2 << std::endl;
CPPTest* pt2 = new CPPTest(1 , 2 , 3); // NOTE:调用在堆上
delete pt2;
return 0;
}总结:当类对象销毁时有一些我们必须手动操作的步骤时,析构函数就排上了用场。所以,几乎所有的类我们都要写构造函数,析构函数未必需要
类的权限修饰#
访问限定符#
C++ 通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限(也成为可见性),分别表示:公有的、受保护的、私有的
class Base {
public:
protected:
private:
};访问权限#
能不能使用该类中的成员
一般地,在类的内部,无论成员被声明为哪种,都是可以互相访问的;但在类的外部,如通过类的对象,则只能访问 public 属性的成员,不能访问 protected、private 属性的成员。
public: 可以被该类中的函数、子类的函数、友元函数访问,也可以由该类的对象访问
protected: 可以被该类中的函数、子类的函数、友元函数访问,但不可以由该类的对象访问
private:可以被该类中的函数、友元函数访问,但不可以由子类的函数、该类的对象访问
private 关键字的作用在于更好地隐藏类的实现
注意事项#
- 如果声明不写
public、protected、private,则默认为private - 声明
public protected public的顺序可以任意 - 在一个类中,
public、protected、private可以出现多次,每个限定符的有效范围到出现另一个限定符或类结束为止。但为了使程序清晰,应该每种限定符只出现一次
this 关键字#
编译器将 this 解释为指向函数所作用的对象的指针
#include <iostream>
class Test {
private:
std::string name;
unsigned old;
public:
Test(const std::string &name_, unsigned old_);
~Test();
void outPut() const {
std::cout << "name = " << name << " old = " << old << " ";
std::cout << "name = " << this->name << " old = " << this->old;
}
};
Test::Test(const std::string &name_, unsigned old_) : name(name_), old(old_) {}
Test::~Test() {}
int main() {
Test test("cxy", 100);
test.outPut();
return 0;
}当然,这么说并非完全正确,this 是一个关键字,只是我们将它当做指针理解罢了
常成员函数、常对象#
在大型程序中,尽量加上 const 关键字可以减少很多不必要的错误
- 常成员函数就是无法修改成员变量的函数,可以理解为将 this 指针指向对象用 const 修饰的函数
- 常对象就是用 const 修饰的对象,定义好之后就再也不需要修改成员变量的值了
常成员函数注意事项:
- 因为类成员函数已将 this 指针省略了,只能在函数后面加 const 关键字来实现无法修改类成员变量的功能了
void outPut()const { std::cout << this->name << ' ' << this->old << std::endl; }- 注意: 常函数无法调用普通函数,否则常函数的这个“常”字还有什么用?
- 成员函数能写作常成员就尽量写作常成员函数,可以减少出错几率
- 同名的常成员函数和普通成员函数是可以重载的,常量对象会优先调用常成员函数,普通对象会优先调用普通成员函数
#include <iostream>
class Test {
public:
Test(const std::string &name_, unsigned old);
~Test();
void outPut() const {
std::cout << "const: " << this->name << ' ' << this->old << std::endl;
}
void outPut() { std::cout << this->name << ' ' << this->old << std::endl; }
private:
std::string name;
unsigned old;
};
Test::Test(const std::string &name_, unsigned old_) : name(name_), old(old_) {}
Test::~Test() {}
int main() {
const Test test("cxy", 1010);
test.outPut();
Test test2("yxc" , 1000);
test2.outPut();
return 0;
}常对象注意事项:
- 常对象不能调用普通函数
- 常函数在大型程序中真的很重要,很多时候我们都需要创建好就不再改变的对象,不要怕麻烦
总结: 常成员函数和常对象要多用
常用关键字#
inline和mutable只要知道就好 default和delete需要掌握
inline#
- 在函数声明或者定义中函数返回类型加上关键字
inline就可以把函数指定为内联函数,关键字inline必须与函数定义放在一起才能使函数成为内联,仅仅inline放在函数声明前不起任何作用 - 内联函数的作用,普通函数在调用时需要给函数分配栈空间以供函数执行,压栈等操作会影响成员运行效率,于是 C++提供了内联函数将函数体放到需要调用函数的地方,用空间换效率
#include <iostream>
class Test {
public:
void outPut();
};
inline void Test::outPut() { std::cout << "hello world" << std::endl; }
int main() {
Test test;
test.outPut();
return 0;
}注意事项:
- inline 关键字只是一个建议,开发者建议编译器将成员函数当做内联函数,一般适合搞内联情况编译器都会采纳建议
总结:
- 使用 inline 关键字就是一种提高效率,但加大编译后文件大的方式,现在很少用了
mutable#
一般来说只有在统计函数调用次数才会用到
mutable 意为可变的,与 const 相对,被 mutable 修饰的成员变量,永远处于可变的状态,即使处于一个常函数中,该变量也可以被更改
#include <iostream>
class Test {
public:
void outPut() const;
mutable unsigned outPutCallcnt = 0;
};
void Test::outPut() const {
++outPutCallcnt;
std::cout << "hello world" << std::endl;
}
int main() { // 统计调用次数
Test test;
test.outPut();
test.outPut();
test.outPut();
test.outPut();
test.outPut();
test.outPut();
test.outPut();
test.outPut();
test.outPut();
test.outPut();
test.outPut();
test.outPut();
test.outPut();
test.outPut();
std::cout << test.outPutCallcnt << std::endl;
return 0;
}注意事项:
mutable是一种万不得以的写法,但一个程序不得不使用mutable关键字时,可以认为这部分程序是一个糟糕的设计mutable不能修饰静态成员变量和常成员变量
总结:
mutalbe关键字是一种没有办法的办法,设计时应该尽量避免
default#
- 在编译时不会生成默认构造函数便于书写
- 也可以对默认复制构造函数,默认的复制运算符和默认的析构函数使用,表示使用的是系统默认提供的函数,可以使代码更加明显
- 现代 C++中,哪怕没有构造函数,也推荐将构造函数用`default`关键字来标记,可以让代码看起来更加直观,方便
#include <iostream>
class Test {
public:
Test(unsigned old_) : old(old_) {}
Test(const Test& test) = default;
Test() = default;
Test& operator=(const Test& test) = default;
~Test() = default;
unsigned old;
};
int main() {
return 0;
}总结:
default关键字还是推荐使用,在现代 C++代码中,如果需要使用一些默认的函数,推荐用default标记出来
delete#
- C++会为程序生成默认构造函数,默认复制构造函数,默认重载赋值运算符很多情况下,我们并不希望这些默认的函数被生成,在 C++11 以前,只能有将此函数声明为私有函数或是将函数只声明不定义两种方式
#include <iostream>
class Test {
public:
Test(unsigned old_) : old(old_) {}
Test(const Test& test) = delete;
Test() = delete;
Test& operator=(const Test& test) = delete;
~Test() = delete;
unsigned old;
};
int main() {
return 0;
}总结:
delete关键字还是推荐使用的,在现代 C++代码中,如果不希望一些函数默认生成,就用delete表示,这个功能还是很有用的,例如单例模式中

