一、基础概念
类:用户自定义的数据类型。
对象:类类型的变量,类的实例。
类的成员:成员变量和成员函数。
成员变量:类中定义的变量。
成员函数:类中定义的函数。
定义类的代码样例:
代码语言:javascript复制class ClassName
{
//members
};
//类定义的右花括号后面必须有分号
类的访问修饰符:
public、private、protected。
public: 类的成员可以被类外部的非成员函数访问。
private: 类的成员可以被同一个类中的成员函数访问,或者被友元函数访问,该修饰符可以禁止一些针对类中成员的高风险操作。
protected: 类的成员可以在子类中被访问。
成员函数可以引用同一个类中的所有成员变量,无论它们用哪种修饰符。
代码语言:javascript复制class ClassName
{
public:
//members
private:
//members
protected:
//members
};
C 编程中,结构体和类的使用方式几乎完全相同。结构体中的成员,可以是变量,也可以是函数。
与类的成员不同的是,结构体中的成员默认是public修饰的,而类中的成员默认是private修饰的。
访问类成员的方法:
用类的对象来访问:对象名.成员名
用类的指针来访问:指针名->成员名
关于成员函数的内存空间:
基于同一个类创建的多个对象,该类的成员函数被多个对象所共享,即类的成员函数在多个对象之间只有一个副本。
二,构造函数
1.关于构造函数
程序在创建对象时,将自动调用构造函数。类的成员变量可以由构造函数来初始化,构造函数与包含它的类同名,没有返回值,也没有返回类型,指定返回类型会导致编译报错。
2.默认构造函数
如果开发者没有给类指定构造函数,编译器会给类定义一个默认的构造函数去调用,编译器生成的默认构造函数,没有参数,只创建对象,给成员变量赋默认值。
程序中没有定义任何构造函数时,编译器会提供默认构造函数。
当程序中已经为一个类提供了非默认构造函数,就必须再定义一个不接受任何传参的默认构造函数。
默认构造函数的定义方式:
方式一,给已有的构造函数的所有参数提供默认值
代码语言:javascript复制Stock(string co = "Error", int n = 0, double price = 0.0)
{
}
方式二,通过函数重载的方式,定义一个没有任何传参的构造函数
代码语言:javascript复制Stock() //重载
{
company = "no name";
shares = 0;
share_val = 0.0;
total_val = 0.0;
}
基于默认构造函数创建对象的方式:
代码语言:javascript复制Stock first;
Stock first = Stock();
Stock *first_ptr = new Stock;
基于非默认构造函数创建对象的方式:
代码语言:javascript复制Stock first("food");
注意,调用默认构造函数,通过隐式的方式创建对象时,不要使用圆括号。
代码语言:javascript复制Stock second(); //返回Stock对象的函数
Stock second; //隐式创建对象
3.构造函数的注意事项
1.不需要被显式调用,由系统调用。
2.无返回值,但是不需要用void修饰。
3.函数声明时,用public修饰。
4.对象在被复制时,调用的不是构造函数,是拷贝构造函数。
5.构造函数可以被重载,一个类可以有多个构造函数。
4.构造函数的初始化列表
写法样例:
代码语言:javascript复制Time::Time(int tmphour, int tmpmin, int tmpsec)
:Hour(tmphour), Minute(tmpmin) //初始化列表
{
Second = tmpsec; //函数体中赋初值
}
初始化列表对变量的初始化顺序是按照变量在类中的定义顺序来操作的,先被定义的先初始化。
系统会先执行初始化列表中的初始化操作,再执行函数体中的代码逻辑。因此,可以在初始化列表中初始化成员变量的值,初始化完成后可以在函数体中修改成员变量的值。
特殊情况:const修饰的成员变量,在初始化列表中初始化以后,不能在函数体中再进行赋值操作。
代码语言:javascript复制Time::Time(int tmphour, int tmpmin, int tmpsec)
:Hour(tmphour), Minute(tmpmin), testValue(30)
{
testValue = 40; //错误操作,不可以在这里修改testValue的初值
}
5.构造函数初始化对象的方式
1、显式调用构造函数
代码语言:javascript复制Stock food = Stock("World Cabbage", 250, 1.25);
2、隐式调用构造函数
代码语言:javascript复制Stock garment("Furry Mason", 50, 2.5);
//这种格式比较紧凑,它与下面的显式调用等价
Stock garment = Stock("Furry Mason", 50, 2.5);
3、创建对象时未提供初始值,系统会调用默认构造函数
代码语言:javascript复制Stock fluffy;
4、构造函数与new一起使用
代码语言:javascript复制Stock *pstock = new Stock("Electroshock Games", 18, 19.5);
该语句创建一个Stock对象,并将该对象的地址赋值给pstock指针。在这种情况下,对象没有名称,但可以使用指针来管理该对象。
5、特殊情况,只有一个参数的构造函数,容易发生隐式的类型转换
如果构造函数只有一个参数,则将对象初始化为一个与参数的类型相同的值时,该构造函数将被调用。
例如,构造函数原型:
代码语言:javascript复制Bozo::Bozo(int num)
{
Num = num;
}
则可以使用下面的任何一种方式来初始化该对象:
代码语言:javascript复制Bozo A_obj = Bozo(44);
Bozo B_obj(66);
Bozo C_obj = 32; //可以使用explicit关键字来关闭这种特性。
6.C 11风格的对象初始化
C 11中可以用{ }来进行对象的初始化:
代码语言:javascript复制Stock hot_tip = {"Derivatives Plus Plus", 100, 45.0};
Stock jock {"Sport Age Storage, Inc"};
Stock temp {};
三,析构函数
1.关于析构函数
类的析构函数总是在释放对象时自动调用。
如果构造函数中使用new来分配内存,则析构函数中必须使用delete来释放这些内存。
在栈内存中先后创建两个对象,最晚创建的对象将最先调用析构来删除,最早创建的对象将最后调用析构来删除。
2.析构函数的注意事项
1.不需要被显式调用,由系统调用。
2.无返回值,但是不需要用void修饰。
3.函数声明时,用public修饰。
4.析构函数没有函数参数,不能被重载,所以一个类只能有一个析构函数。
5.如果开发者在构造函数里面new了一段内存,此时需要自定义一个析构函数,并在析构函数中调用delete方法将这段内存释放掉。
对于指针类型的成员变量,在考虑析构问题时,有两个编程技巧:
忘记使用delete释放对象——使用智能指针std::unique_ptr进行封装。
不知道什么时候释放由多个对象指向或使用的同一个对象——使用智能指针std::shared_ptr进行封装。
四,创建对象:堆内存 & 栈内存
如果对象只在一个函数中被使用,且该对象被使用的时间很短,并且从创建该对象的函数return后不再需要该对象,推荐在栈内存中创建该对象。
如果对象必须在多个函数之间使用,且该对象被使用的时间很长,推荐在堆内存中创建该对象。
栈内存中创建对象的语法:
代码语言:javascript复制<ClassName> <object name>;
代码样例:
代码语言:javascript复制Rectangle obj;
堆内存中创建对象的语法:
代码语言:javascript复制<ClassName> *<object name> = new <ClassName>;
代码样例:
代码语言:javascript复制Rectangle *obj = new Rectangle();
五,const对象 & const成员函数
1.const对象
const对象的所有成员变量都是const类型,不能被修改。
当通过const指针或const引用访问对象时,具有与直接访问const对象相同的限制。
对于const对象,只能调用const成员函数。
const修饰的成员函数,就是在告诉开发者,该const对象的哪些成员函数可以被调用。一般只对getter函数用const修饰,对setter函数用const修饰会导致编译报错。
2.const成员函数
只要类的成员函数不修改对象的成员变量,就应该将其声明为const。
代码样例:
代码语言:javascript复制void Stock::show() const //const放函数名后面
3.mutable关键字
const对象中,被mutable关键字修饰的成员变量仍可以被修改,且可以同时被const成员函数和非const成员函数所修改。
代码语言:javascript复制class Screen{
public:
void some_member() const;
private:
mutable size_t access_ctr; //在const对象内也能被修改
};
void Screen::some_member() const
{
access_ctr; //保存一个计数值,用于记录成员函数被调用的次数
}
六,成员函数的this指针
this是当前对象的地址。
返回对象本身需要进行解引用操作,即return *this,返回的是调用该成员函数的对象。
成员函数通过this指针来访问调用它的整个对象,而不是直接访问对象的某个成员。
正常情况下,this的类型是指向对象的常指针,const成员函数相当于把this声明为指向不可变对象的常指针。
return this的函数返回值:返回值是对象 & 返回值是对象的引用。
返回值是对象:改变的是同一个对象。
返回值是对象的引用:改变的不是同一个对象,而是对象的副本。
代码样例,假设有一个对象myBox:
代码语言:javascript复制class Box
{
private:
double length {1.0};
double width {1.0};
double height {1.0};
public:
...
}
Box myBox{3.0, 4.0, 5.0};
场景1:返回值是对象
成员函数:
代码语言:javascript复制Box* Box::setLength(double lv)
{
if(lv > 0)
{
length = lv;
}
return this;
}
Box* Box::setWidth(double wv)
{
if(wv > 0)
{
width = wv;
}
return this;
}
Box* Box::setHeight(double hv)
{
if(hv > 0)
{
height = hv;
}
return this;
}
要修改同一个对象的所有成员变量,调用方法:
代码语言:javascript复制myBox.setLength(20.0)->setWidth(30.0)->setHeight(40.0);
//Set all dimensions of myBox
场景2:返回值是对象的引用
成员函数:
代码语言:javascript复制Box& Box::setLength(double lv)
{
if(lv > 0)
{
length = lv;
}
return *this;
}
Box& Box::setWidth(double wv)
{
if(wv > 0)
{
width = wv;
}
return *this;
}
Box& Box::setHeight(double hv)
{
if(hv > 0)
{
height = hv;
}
return *this;
}
要修改同一个对象的所有成员变量,调用方法:
代码语言:javascript复制myBox.setLength(20.0).setWidth(30.0).setHeight(40.0);
//Set all dimensions of myBox
七,关于对象的动态内存分配
1.如果对象是动态变量,则当执行完定义该对象的代码块时,就会调用该对象的析构函数。
2.如果对象是静态变量,则在整个程序运行结束时,才调用该对象的析构函数。
3.如果对象是用new创建的,则仅当显式调用delete删除对象时,才调用该对象的析构函数。
具体的区别参考《c primer plus》的这张图
八,对象的生命周期
1.对象的生命周期——创建对象
以下操作会创建对象:
1.在栈内存中声明对象。
2.使用new、new[]显式分配空间。
3.使用智能指针显式分配空间。
*特殊情况,创建对象的同时,创建一个内嵌的对象
代码语言:javascript复制#include <string>
class MyClass
{
private:
std::string mName;
};
int main()
{
MyClass obj;
return 0;
}
基于MyClass类创建obj对象的时候,同时会创建一个内嵌的string对象,当包含string对象的obj对象被销毁时,string对象也被一起销毁。
2.对象的生命周期——销毁对象
销毁对象时,系统会进行的操作:调用对象的析构函数,释放对象占用的内存。
析构函数中的常见操作:释放动态分配的内存、关闭文件句柄。
对象的析构顺序与声明对象时的初始化顺序相反,最先被初始化的对象,最后被析构。
栈内存中的对象销毁:
当栈内存中的对象超出作用域以后,对象会被自动销毁。
对于一段代码,当代码遇到结束时的大括号时,这个大括号内所有创建在栈内存中的对象会被自动销毁。
例如,以下代码中,对象是创建在栈内存中的,会自动销毁。
代码语言:javascript复制int main()
{
MyClass myCell("a");
...
return 0;
} //myCell is destroyed as this block ends.
堆内存中的对象销毁:
如果没有使用智能指针,在堆内存中创建的对象,不会被自动销毁。
必须调用delete或delete[]删除对象指针,从而调用析构函数释放内存。
例如,以下代码中,对象是创建在堆内存中的,不会自动销毁。
代码语言:javascript复制int main()
{
MyClass* objPtr1 = new MyClass("b");
MyClass* objPtr2 = new MyClass("c");
...
delete objPtr1;
objPtr1 = nullptr;
return 0;
} //objPtr2 is not destroyed.
九,类的静态成员
1.静态成员变量
当类的成员变量被声明为static类型时,该变量被称为类的静态成员变量。
类的静态成员变量作用于整个类,独立于任何类的对象。该类的所有对象都可以访问这个静态成员变量。
静态成员变量可以作为类的特殊全局变量,它可以用来存储关于类的具体信息,比如当前类有多少个对象等。
该类即使没有被实例化为对象,它的静态成员变量依然存在。
静态成员变量,在多个类对象之间共享访问,只定义一次。
在创建对象时,对象的普通成员变量会在每个对象中拷贝一个独立的副本。如果对象的某个成员变量的值是个常量,则创建多个对象还得为这个常量生成多个副本,很浪费内存空间,如果将该成员变量声明为静态成员变量,则该成员变量会被多个对象所共享,且在创建很多对象的期间只有一个实例,不会产生多个副本。
2.静态成员函数
当类的成员函数被声明为static类型时,该函数被称为类的静态成员函数。
类的静态成员函数也作用于整个类,独立于任何类的对象,该类的所有对象都可以调用这个静态成员函数。
注意,由于静态成员函数与具体的对象无关,所以静态成员函数不能用const修饰,也不能使用this指针。
如果静态成员函数被声明为public,还可以在类的外部被调用。
十,参考阅读
《C 高级编程》
《C 17入门经典》
《C 新经典》
《C Primer》
《C Primer Plus》