2. 类、对象、成员函数的介绍
2.1 类的基本概念
在上一节中,讨论了类,对象,数据成员(属性),成员函数(行为)。有日期对象、时间对象、音频对象、视频对象、汽车对象、人对象等。几乎任何名词都可以在属性(如名称、颜色和大小)和行为(如计算、移动和通信)方面合理地表示为软件对象。
可以将汽车比喻为类,汽车可以完成的任务就是成员函数。在C++中,我们经常创建一个称为类的程序单元来容纳一组函数执行类的任务——这些任务被称为类的成员函数。例如代表银行帐户的类可能包含用于存款的成员函数给一个账户,另一个从账户提款,第三个查询账户的当前余额是。一个类类似于一辆汽车的工程图纸,哪个房子设计了加速踏板、制动踏板、方向盘等。
就像有人必须根据工程图纸制造汽车一样驾驶汽车时,必须先从类中构建一个对象,然后程序才能执行任务类的成员函数定义的。这样做的过程称为实例化。然后,一个对象被称为其类的实例。
就像汽车的工程图纸可以多次重复使用来制造许多汽车一样,你可以多次重用一个类来构建许多对象。在构建新类和程序时重用现有类可以节省时间和精力。重用还可以帮助您构建更可靠、更有效的系统。
当你驾驶汽车时,踩下油门会向汽车发送执行任务的信息--也就是说,走得更快。类似地,您向对象发送消息。每个消息都实现为成员函数调用,告诉对象的成员函数执行其任务。例如,程序可能会调用特定银行帐户对象的存款成员函数来增加帐户余额。
汽车除了具有完成任务的能力外,还具有一些属性,如颜色、车门数量、油箱中的汽油量、当前速度和行驶总里程记录(即里程表读数)。与它的功能一样,汽车的属性也作为其设计的一部分在其工程图中表示(例如,包括里程表和燃油表)。当你驾驶一辆真正的汽车时,这些属性会随汽车一起携带。每辆车都有自己的特点。例如,每辆车都知道自己油箱里有多少油,但不知道其他车油箱里有多油。
类似地,一个对象在程序中使用时也会附带一些属性。这些属性被指定为对象类的一部分。例如,银行帐户对象具有余额属性,表示帐户中的金额。每个银行帐户对象都知道它所代表的帐户中的余额,但不知道银行中其他帐户的余额。属性由类的数据成员指定。
类将属性和成员函数封装(即包装)到从创建的对象中这些类——对象的属性和成员函数是密切相关的。对象可以相互通信,但通常不允许它们知道其他对象是如何实现的——实现细节隐藏在对象本身中。正如我们将看到的,这种信息隐藏对于良好的软件工程至关重要。
一个新的对象类可以通过继承快速方便地创建——类吸收了现有类的特性,可能会对其进行自定义并添加独特的特点。在我们的汽车类比中,一个“敞篷”类的物体当然是更普通的“汽车”的一个对象,但更具体地说,车顶可以升高或降低。
类包含:
1.由变量定义的数据域
2.由函数定义的行为
类的名字必须首字母大写。
2.2 对象的构成
对象是类的实例,具有唯一的标识、状态和行为:
1.对象状态由数据域(也称为属性)及其当前值构成
2.对象的行为由一组函数定义
在你创造一个类的对象之前,不可以调用类中的成员函数。
2.3 类:数据成员、set和get函数
2.3.1 类的定义
// Account class that contains a name data member
// and member functions to set and get its value.
#include <string> // enable program to use C++ string data type
class Account {
public:
// member function that sets the account name in the object
void setName(std::string accountName) {
name = accountName; // store the account name
}
// member function that retrieves the account name from the object
std::string getName() const {
return name; // return name's value to this function's caller
}
private:
std::string name; // data member containing account holder's name
}; // end class Account
上述代码即为Account类的定义。
2.3.2 关键词class和class body
1.在类的定义中,结束的大括号后面要写上分号。忘记类定义末尾的分号是语法错误。为了较好的可复用性,可以将类的定义单独写为一个.h文件。
2.在C++中,需要对类、对象和函数等命名,若名称中间没有空格和标点,除第一个单词外后面的单词首字母均大写。称为驼峰式命名。
如果第一个单词首字母大写,称之为upper camel case
(CamelCase
,大驼峰式),例如"GetUserName"
。
如果第一个单词首字母小写,称之为lower camel case
(camelCase
,小驼峰式),例如"getUserName"
。
2.3.3 类数据成员
数据成员存在于:
1.在程序调用对象的成员函数之前
2.在成员函数执行之时
3.在成员函数完成执行之后
数据成员的声明在类的定义内,但在类的成员函数体之外声明。如上诉Account类的定义。按照惯例,一般将数据成员的定义放在class body的最后。
2.3.4 函数形参
在函数的形参中,每个形参必须指明类型。当一个函数有多个形参时,每个形参之间用逗号隔开。实参的个数和顺序必须和形参的个数顺序相同。函数的形参是局部变量。
实参和形参的类型必须一致,但不必相同
2.4 使用构造函数初始化对象
2.4.1 构造函数
构造函数是特殊的成员函数,必须和类的名字一样。C++在每个对象建立时,都要调用构造函数。像成员函数一样,构造函数也可以有形参,对应的实参值帮助初始化对象的数据成员。每个类都可以定义一个构造函数,该构造函数为该类的对象指定自定义初始化。
构造函数的特点:
1.在创建对象时,自动的进行初始化工作。
2.构造函数在声明时,前面不能加返回类型,void也不可以。
3.构造函数与类同名。
4.没有返回值。
5.可以重载构造函数。
6.构造函数可以不带参数。
默认构造函数:调用时可以不需要实参的构造函数。即参数表为空的构造函数或全部参数都有默认值的构造函数。
Clock(){
//class body
}//默认构造函数,无参数,函数体可能为空
Clock(int n=0,int x=1,double y=0){
//class body
};//默认构造函数,默认参数,函数体可能为空
上述两种形式的构造函数如果同时在类中出现,将会产生编译错误。
默认构造函数的角色:
1.若内嵌对象成员没有被显式的初始化,该内嵌对象的无参构造函数会被自动调用。若内嵌对象没有无参构造函数,则编译器会报错。
class X{
private:
Circle c1;//c1即为内嵌对象
public:
X() {}
};
2.也可以在初始化列表中手工构造对象。参见2.7节。
若类的数据域是一个对象类型(且它没有无参构造函数),则该对象初始化可以放到类的构造函数初始化列表中。
创建对象:
Circle circle1;//正确,但是不推荐这么写
Circle circle();//错误,编译器会认为这是一个函数声明
Circle circle3{};//正确,推荐写法。这里明确显式用空初始化列表初始化Circle3对象(调用Circle的默认构造函数)
类可以不声明构造函数,此情况下,编译器会提供一个带有空函数体的无参构造函数
1.一个类可以有多个构造函数
class Student
{
Student() {};//默认构造函数 default constructor
Student(int age) {};//非默认构造函数
Student(int age, bool sex) {};//非默认构造函数
};
int main()
{
Student stu1;//调用默认构造函数
Student stu2(10);//调用带一个整形参数的构造函数
Student stu3(10, true);//调用两个参数的那个构造函数
return 0;
}
2.编译器合成的默认构造函数
如果我们一个构造函数都没有定义,编译器也会为我们合成一个默认构造函数(default constructor)。
class Student
{
};
int main()
{
Student stu1;//调用编译器合成的默认构造函数(虽然我们没有看见那个函数在哪,编译器真是太热心了)
return 0;
}
3.禁止编译器合成默认构造函数
只要我们自己定义了构造函数,编译器就不会再多管闲事替我们热心的合成一个默认的构造函数了。
下面的类已经没有默认构造函数了,因为我们定义了非默认构造函数,编译器不再替我们合成,而我们自己也没有定义默认构造函数。
class Student
{
Student(int age) {};//非默认构造函数
Student(int age, bool sex) {};//非默认构造函数
};
int main()
{
Student stu2(10);//调用带一个整形参数的构造函数
Student stu3(10, true);//调用两个参数的那个构造函数
return 0;
}
这样的类也有一个不便之处,那就是像下面这样定义对象会报错:
如果要解决这个问题,就只能像一开始那样,自己显式的定义一个默认构造函数。
如果类中已经定义构造函数,默认情况下编译器就不再隐含生成默认构造函数。如果此时依然希望编译器隐含生成默认构造函数,可以使用"=default"
class Clock{
public:
Clock()=default;//指示编译器提供默认构造函数
CLock(int n,intx,int y);//构造函数
private:
int hour,minute,second;
}
Clock(){}
使用上述形式的构造函数也可以满足需求,但是上述形式的构造函数在每次生成对象时都会调用,使用default只要在需要的时候才会调用。
2.4.2 对象访问运算符
使用点运算符访问对象中的数据和函数。
Circle circle1;
circle.radius=10;
int area =circle1.getArea();
2.4.3 explicit关键词
指定单个参数的构造函数应声明为explicit
2.4.4 在对象创立时初始化对象
#include <iostream>
#include "Account.h"
using namespace std;
int main(){
Account account1{"Jane Green"};
Account account2{"John Blue"};
cout<<"account1 name is: "<<account1.getName()<<endl;
cout<<"account2 name is: "<<account2.getName()<<endl;
}
在任何没有明确定义构造函数的类中,编译器都会提供一个没有参数的默认构造函数。默认构造函数不会初始化类的基本类型数据成员,但会为另一个类的对象的每个数据成员调用默认构造函数。例如在如下的代码中:
class Account{
public:
void setName(std::string accountName){
name=accountName;
}
std::string getName() const{
return name;
}
private:
std::string name;
}
上述例中的类的默认构造函数调用了string类的默认构造函数初始化名为name的数据成员为空的字符串。
一个未被初始化的基本类型变量包含一个未定义的值
如果你定义了一个传统的(custom)的构造函数在类中,编译器不会为类生成一个默认构造函数。在这种情况下,就不可以使用如下的语句创造一个Account对象,除非你定义的构造函数有一个空的形参列表。后续我们会知道,在C++11的标准中,允许你强迫编译器创造一个默认的构造函数即使你已经定义了非默认函数。除非类的数据成员的默认初始化是可接受的,否则通常应该提供一个自定义构造函数,以确保在创建类的每个新对象时,数据成员都以有意义的值进行了正确的初始化。
Account myAaccount;
2.5 类的示例
balance(余额)
2.5.1 UML类图
#include <string>
class Account {
public:
// constructor initializes data member name with parameter accountname
Account(std::string accountName, int initialBalance)
:name{accountName}{
if (initialBalance>0){
balance=initialBalance;
}
}
void deposit(int depositAmount){
if(depositAmount>0){
balance=balance+depositAmount;
}
}
int getBalance() const{
return balance;
}
void setName(std::string accountName){
name=accountName;
}
std::string getName() const{
return name;
}
private:
std::string name;
int balance{0};
};
在上述代码中,即使变量balance没有在任何一个成员函数中声明,所有的成员函数仍然可以使用balance,我们可以在这些成员函数中使用 balance因为它是同一个类定义中的数据成员。
上诉类的UML类图为:
2.6 匿名对象
有时候需要创建一个只用一次的对象,此时,无需给对象命名,这种对象叫做匿名对象。匿名对象用在成员拷贝,如下例:
Classname();//无参构造函数
Classname(arguments);//有参构造函数
#include <iostream>
using namespace std;
class Circle{
public:
double radius;
Circle(){
radius=1;
}
Circle(double newRadius){
radius=newRadius;
}
double getAera(){
return radius*radius*radius;
}
};
int main(){
Circle circle1,circle2;
circle1=Circle();
circle2=Circle(5);
cout<<"area is"<<circle1.getAera()<<endl;
cout<<"area is"<<circle2.getAera()<<endl;
cout<<"area is"<<Circle().getAera()<<endl;
cout<<"area is"<<Circle(5).getAera()<<endl;
}
2.7 构造函数初始化列表
在构造函数中用初始化列表初始化数据域。
className (parameterList)
:dataField1{value1},dataField2{value2}//这一行就是初始化列表
{
//do something
}
对于基础类型,下列两种写法效果相同。
Circle::Circle():radius{1}{
}
Circle::Circle(){
radius=1;
}
使用构造函数初始化列表的原因:
因为在类的数据域中可能存在一个对象类型,此对象被称为对象中的对象,或者内嵌对象。
内嵌对象必须在被嵌对象的构造函数体执行完成前就构造完成。因此使用构造函数初始化列表
考虑下列代码:
class Time{
//code omitted
};
class Action{
public:
Action(int hour,int minute,int second){
time=Time(hour,minute,second)
}
private:
Time time;
};
在Action类中,构造函数的函数体中,对time对象执行赋值操作(使用Time创建的匿名对象)。也就是说在构造函数的函数体执行之前,time对象就要存在了,不存在的话就无法进行赋值操作。那么time对象在何处创建并初始化的呢?应该在函数体执行之前构造并初始化完成。即使用构造函数的成员初始化列表进行time的初始化。
若类的数据域是一个对象类型(且它没有无参构造函数),则该对象初始化必须放在初始化列表内。
2.8 不可变对象
不可变对象,即对象创建后,其内容不可改变,除非通过成员拷贝
不可变类:不可变对象所属的类
让类变为不可变类的方法:
1.所有数据域均设置为private属性
2.没有更改器函数
3.也没有能够返回可变数据域对象的引用或指针的访问器
多线程编程和函数式编程会用到不可变类。
2.9 类的前向引用声明
类应该先声明,后引用
如果需要在某个类的声明之前,引用该类,则应进行前向引用声明
前向引用声明只为程序引入一个标识符,但具体声明在其他地方
2.10 常量(Constant)/const关键字
1.常量是程序中的一块数据,这个数据一旦声明后就不能修改了。
如果这块数据有一个名字,这个名字就叫做命名常量。比如const int A=42,A就是命名常量
如果这个常量从字面上看就可以知道它的值,那它就叫做“字面值常量”,例如上例中的42即为字面值常量。
2.符号常量(包括枚举值)必须全部大写并用下划线分隔单词
例如:MAX_ITERATIONS,COLOR_RED,PI
通过下列方式定义一个常量:
const datatype CONSTANTNAME = VALUE;//const和数据类型可以互相交换
const double PI=3.14159;
const int SIZE=3;
int const X=5;
const int C=3;
const char C='k';
const char* STR="hello";
上述代码中,int const和const int 等价。
PI、SIZE等就叫做命名常量或者符号常量
2.10.1 常量表达式(constant expressions)
1.常量表达式是编译期i可以计算值的一个表达式。
例如,C++数组的大小要求是编译期的一个常量(原生数组以及array)
int n=1;
n++;
array<int,n> a1;//error!n不是一个常量表达式
const int x=2;
array<int,x> a2;//正确!n是一个常量表达式
2.const修饰的对象未必是编译期常量
const int rcn=n;//rcn是一个运行期常量,编译器在编译期不知道它的值
rcn=++n;//
array<int,rcn> a3;//
2.10.2 constexpr:编译期常量表达式说明符
constexpr说明符声明可在编译时计算函数或变量的值
constexpr int max(int a , int b) { // c++11 引入 constexpr
if (a > b)
return a; // c++14才允许constexpr函数中有分支循环等
else
return b;
}
int main() {
int m = 1;
const int rcm = m++; // rcm是运行期常量
const int cm = 4; // 编译期常量,等价于: constexpr int cm = 4;
int a1[ max(m , rcm)]; // 错误:m & rcm 不是编译期常量
std::array<char , max(cm , 5)> a2; // OK: cm 和 5 是编译期常量
}
constexpr函数可以接受非常量实参,但此时的结果不再是一个常量表达式。
2.10.3 const vs constexpr
1.主要区别
const:告知程序员,const 修饰的内容是不会被修改的。主要目的是帮程序员避免bug 。
char* s1 = "Hello"; // C语言允许,但C++编译出错
*s1 = 'h’; // C语言中,语法正确,但运行时会出错
const char* s2 = "World"; // C++ 要求加const
*s2 = 'w'; // C++编译器报错
constexpr :用在所有被要求使用“constant expression”的地方(就是constexpr修饰的东西可以在编译期计算得到值),主要目的是让编译器能够优化代码提升性能 。
2.constexpr:初学者只需了解其含义即可
constexpr用法有非常多的细节(cppreference.com 列出了30 多个条目)
C++14 、C++17 、C++20 对它都有细节修改
2.10.4 字面值(字面量)
C++中字面值常量是一类特殊的常量,它们没有名字,只能用它们的值来称呼,因此得名“字面值常量”。常见的字面值常量包括以下几类:
- 整型字面值常量:1,2,3,4,5等等
- 浮点型字面值常量:1.1,2.2,3.3等等
- 布尔类型字面值常量:true,false
- 字符字面值常量:'a','b','c','d'等等
- 字符串字面值常量:"abc","def"等等
其中只有字符串字面值常量存储在全局区,可以取地址,其他的字面值常量都放在寄存器上,不能取内存地址。
比起字面值常量,使用const等定义的常量有一个可以称呼的名字,如const int a=2;
名字就是a
2.10.5 C++14 字符串字面量
- C++11 Raw String literals (C++11“原始/生”字符串字面量)
从名字上就可以看出,这种“Raw String literals”应该长得很原始。那么这个原始该怎么体现出来呢?我们通过语法和简单示例就能看出来。
语法:
R"delimiter( raw_characters )delimiter"
#include <iostream>
const char* s1 = R"(Hello
World)";
// s1效果与下面的s2和s3相同
const char* s2 = "Hello\nWorld";
const char* s3 = R"NoUse(Hello
World)NoUse";
int main(){
std::cout << s1 << std::endl;
std::cout << s2 << std::endl;
std::cout << s3 << std::endl;
}
从例子中看出,“Raw String literals”在程序中写成什么样子,输出之后就是什么样子。我们不需要为“Raw String literals”中的换行、双引号等特殊字符进行转义。
- C++14: String Literals (C++14的字符串字面量)
C++14将运算符 ""s 进行了重载,赋予了它新的含义,使得用这种运算符括起来的字符串字面量,自动变成了一个 std::string 类型的对象。
auto hello = "Hello!"s; // hello is of std::string type
auto hello = std::string{"Hello!"}; // equals to the above
auto hello = "Hello!"; // hello is of const char* type
例子:
#include <string>
#include <iostream>
int main() {
using namespace std::string_literals;
std::string s1 = "abc\0\0def";
std::string s2 = "abc\0\0def"s;
std::cout << "s1: " << s1.size() << " \"" << s1 << "\"\n";
std::cout << "s2: " << s2.size() << " \"" << s2 << "\"\n";
}
上面例子的一个可能输出结果如下:
2.11 用指针访问对象成员(对象指针)
对象指针可以指向新的对象名。箭头运算符->,使用指针访问对象成员。
Circle circle;
Circle* pCircle=&circle1;
cout<<(*pCircle).radius<<endl;
cout<<(*pCircle).getArea<<endl;
(*pCircle).radius=5.5;
cout<<pCircle->radius<<endl;
cout<<pCircle->getArea()<<endl;
2.12 C++成员的就地初始化
非静态成员可以就地初始化。
在类中进行数组的就地初始化,编译器不能进行数组大小的自动推断。
class X{
int a[] {1,4,5};//错误!
int a1[3] {1,4,5};//正确!
};
2.13 类的成员的初始化次序
执行次序:就地初始化->构造函数初始化列表->在构造函数体中为成员赋值
初始化优先级:在构造函数体中为成员赋值->构造函数初始化列表->就地初始化
若一个成员同时就地初始化和构造函数列表初始化,则就地初始化语句被忽略不执行。
2.14 断言(Assertion)与静态断言
断言是一条检测假设成立与否的语句。如果假设成立,断言不会产生作用,如果假设不成立,断言会终止程序的运行。
1.1. assert :C语言的宏(Macro),运行时检测。
用法:
//包含头文件 <cassert> 以调试模式编译程序
assert( bool_expr ); // bool_expr 为假则中断程序
std::array a{ 1, 2, 3 }; //C++17 类型参数推导
for (size_t i = 0; i <= a.size(); i++) {
assert(i < 3); //断言:i必须小于3,否则失败
std::cout << a[ i ];
std::cout << (i == a.size() ? "" : " ");
}
1.2. assert()依赖于NDEBUG 宏
NDEBUG这个宏是C/C++标准规定的,所有编译器都有对它的支持。
(1) 调试(Debug)模式编译时,编译器不会定义NDEBUG,所以assert()宏起作用。
(2) 发行(Release)模式编译时,编译器自动定义宏NDEBUG,使assert不起作用
如果要强制使得assert()生效或者使得assert()不生效,只要手动 #define NDEBUG 或者 #undef NDEBUG即可。
1.3. assert 帮助调试解决逻辑bug (部分替代“断点/单步调试”)
#undef NDEBUG // 强制以debug模式使用<cassert>
int main() {
int i;
std::cout << "Enter an int: ";
std::cin >> i;
assert((i > 0) && "i must be positive"); //&&是一个非空指针,一定是真的。
return 0;
}
上面示例的第6行代码中,若assert中断了程序则表明程序出bug了!程序员要重编代码解决这个bug,而不是把assert()放在那里当成正常程序的一部分
2.C++11:static_assert (C++11的静态断言 )
2.1. static_assert ( bool_constexpr, message)//C++17起,message可选
其中两个参数解释如下:
(1) bool_constexpr: 编译期常量表达式,可转换为bool 类型
(2) message: 字符串字面量 ,是断言失败时显示的警告信息。自C++17起,message是可选的
2.2. 作用:编译时断言检查
// 下面的语句能够确保该程序在32位的平台上编译进行。
// 如果该程序在64位平台上编译,就会报错
static_assert(sizeof(void *) == 4, "64-bit code generation is not supported.");
2.3. static_assert的用途
常用在模版编程中 ,对写库的作者用处大
在static_assert的第一个参数 bool_constexpr 中不能有变量表达式
3.何时使用断言
若某些状况是你预期中的,那么用错误处理;若某些状况永不该发生,用断言
int n{ 1 } , m{ 0 };
std::cin >> n;
assert((n != 0) && "Divisor cannot be zero!"); // 不合适
int q = m / n;
int n{ 1 } , m{ 0 };
do { // 这是修补bug的代码
std::cin >> n; // 断言失败后,要解决这个bug
} while (n == 0); // 在这里编写修复bug的代码
assert((n != 0) && "Divisor cannot be zero!");
int q = m / n;
2.15 不可变对象和类
不可变对象:对象创建后,其内容不可改变,除非通过成员拷贝,对于编写多线程程序很有帮助。
不可变类:不可变对象所属的类。
如何让一个类成为不可变类:
1.所有数据域均设置为private属性
2.没有更改器函数
3.没有能够返回可变数据域对象的引用或指针的访问器
标签:02,初始化,const,函数,常量,int,成员,对象,构造函数 From: https://www.cnblogs.com/yyyylllll/p/18388532