C++类和对象

C++中的类

类是一种将抽象转化为用户定义类型的C++工具,它将数据表示和操纵数据的方法组合成一个整洁的包,例如定义一个股票的类,将股票的信息和操纵方法组合为一个整洁的包,那么这个包里久需要包含股票的信息,即是哪一个公司的股票,每一股的价格是多少等,这就是数据部分,这个包里还需要包含对股票的操作部分,即获得股票,卖出股票等,这就是操作数据的方法

定义类

一般来说,类由两个部分组成

类声明:以数据成员的方式描述数据部分,以成员函数(方法)的方式描述公有接口

class 类的名字

{

        //私有部分

        private:

                        成员定义;

        //公有部分

        public:

                        成员定义;

}

类方法定义:描述如何实现类成员函数

即类的声明提供了类的蓝图,而类方法定义提供了细节,类似于函数声明和定义

通常,C++程序员会将类定义(接口)放在头文件中,将实现类方法的代码放在源文件中,例如,在头文件中对类进行声明Stock类

这里将类定义在头文件中stock00.h中

#ifndef __STOCK00__
#define __STOCK00__

#include<string>
//定义一个类
//使用关键字class,后续跟类名
class Stock
{
        //这里出现的标识,只能通过public公用的成员才能进行访问
        private:
                std::string company; //公司名
                long shares;         //股票数量
                double share_val;    //股票价格
                double total_val;    //总共的价值
                void set_tot()       //计算总价
                {
                        total_val = shares * share_val;
                };
        public:
                void acquire(const std::string &co, long n, double pr );
                void buy(long num, double price);
                void sell(long num, double price);
                void update(double price);
                void show(void);
};



#endif
~                 

我们要使用Stock中public的函数,就要创建Stock对象,只有通过类的对象才能去使用类成员函数,public里面放的是接口函数

访问控制

private,public

这两个关键字描述了类成员的访问控制,使用类对象的程序都可以直接访问公有部分,但只能通过公有成员函数或者友元函数来访问对象的私有成员,例如我们想要去修改shares这个成员,只有通过程序函数才能实现

无论类成员是数据成员还是成员函数,都可以在类的公有部分或者私有部分中声明它,但是由于隐藏数据时OOP主要的目标之一,因此通常将数据项放在私有部分,组成类接口的成员函数放在公有部分,像Stock类一样,我们也可以将类成员函数放在私有部分,我们虽然不能直接去调用它,但是公有办法却可以去使用,通常使用程序员使用私有成员函数来处理不属于公有接口的实现细节

因此C++是默认访问控制,因此我们也可以省略private,即如下

 类成员函数的实现

类成员函数与普通的成员函数相似,但是有特殊的地方

定义类成员函数时,使用作用域解析运算符(::)来标识函数所属的类

类方法可以使用类的private组件

例如新建立一个源文件,实现类成员函数的定义

#include"stock00.h"
#include<iostream>

//公司名称,股票数,价格
void Stock::acquire(const std::string &co, long n, double pr)
{
	company = co;
	if(n < 0)
	{
		std::cout<<"Number of shares can't be negative "<<company<<std::endl;
	}
	else
	{
		shares = n;
	}
	share_val = pr;
	set_tot();
}
//购买数量
void Stock:: buy(long num, double price)
{
	if(num < 0)
	{	
		std::cout<<"Number of shares can't be negative "<<company<<std::endl;
	}	
	else
	{
		shares += num;
		share_val = price;
		set_tot();
	}
}
//卖出
void Stock::sell(long num, double price)
{
	if(num < 0)
	{
		std::cout<<"Number of shares can't be negative "<<company<<std::endl;
	}
	else if(num > shares)
	{
		std::cout<<"You can't sell more than you have!"<<std::endl;
	}
	else
	{
		shares -= num;
		share_val = price;
		set_tot();
	}
}
//更新价格

void Stock::update(double price)
{
	share_val = price;
	set_tot();
}
//显示

void Stock::show(void)
{
	std::cout<<"company: "<<company<<std::endl;
	std::cout<<"shares: "<<shares<<std::endl;
	std::cout<<"share price: "<<share_val<<std::endl;
	std::cout<<"total worth:"<<total_val<<std::endl;
}

这表示方法意味着我们定义的show函数是Stock类的成员,这不仅将show标识为成员函数,还意味着我们也可以将另一个类的成员函数也命名为show,即 void Buff :: show(),因此,作用域解析运算符确定了方法定义所对应的类的身份,我们说show()具有类作用域,Stock类的其他成员函数不需要使用作用域解析运算符就可以使用show()方法,因为它们同属于一个类,因此show是可见的,但是,在类声明和方法定义之外使用show()时,需要使用特殊的方法

注意:

定义位于类声明中的函数将会自动成为内联函数

例如上图中的set_tot()函数,直接将函数定义在了类的声明中,因此该函数会自动成为一个内联函数,而其他的函数没有在类声明中进行定义,因此其他函数不会进行自动转换

我们也可以在类声明之外定义类成员函数,并使其称为内联函数,只需要在类实现部分定义函数时使用inline限定符即可

我们可以通过类来创造对象,如何通过类的对象去访问类的成员函数

#include"stock00.h"
#include<iostream>

int main(void)
{
	//使用类来创建对象
	//通过类的对象来访问类成员函数
	Stock cat;
	cat.acquire("AAA",10,22.1);
	cat.show();
	return 0;
}

在这里我们通过Stock类定义了一个对象,该对象名为cat,通过(.)运算符就可以去访问类的成员函数show()

类的构造函数和析构函数

构造函数

对于类而言,我们应该为类提供称为构造函数和析构函数的标准函数,原因如下

C++的目标之一是让使用类对象就行像使用标准类型,但是常规的初始化语法并不使用于我们上面定义的Stock类,因为对于类的对象而言,数据部分的访问状态是私有的,这意味着我们不能直接访问数据成员,程序只能通过成员函数去访问数据成员,因此需要合适的成员函数,才能成功的将对象进行初始化

一般来说,我们都是在创建对象时,通过使用类成员函数对其进行初始化,但是这样可能会导致某些私有变量没有进行初始化,为了避免这个问题,就需要在创建对象时,自动的对其进行初始化

在这段代码中,我们通过创建Stock类的对象cat来调用了类成员函数,如果类对象cat对shares,share_val, total_val进行了初始化,但是此时却并没有对company进行初始化,此时company是没有值的,为了解决这个问题,C++提供了一个特殊的成员函数——类构造函数

类构造函数专门用于构造新对象,将值赋给它们的数据成员,即C++为这些成员函数提供了名称和使用语法,而程序员需要提供方法的定义

构造函数的名称与类的名称一致,但是构造函数没有返回值,也没有被声明为void类型,即构造函数没有声明其类型

声明构造函数:

定义构造函数:

使用构造函数可以隐式的调用,也可以进行显示的进行调用

在这个代码中,我们进行了构造函数的声明以及定义,并且展示了构造函数的调用方法

注意:

构造函数的参数名不能是类成员名称,因为构造函数的参数表示的不是类成员,而是赋值给类成员的值

默认的构造函数

当我们没有为程序提供任何的构造函数时,C++将自动提供默认构造函数,它是默认构造函数的隐式版本,不做任何的工作,如果当我们程序中定义了Stock类,但并没有给出构造函数,当我们使用类来创建对象时,Stock obj;

此时会使用隐式的构造函数,但是不会为类的数据成员进行初始化,默认构造函数可能如下

Stock :: Stock()

{

}

此时相当于int a;此时a没有值

默认构造函数没有参数,因为声明中不包含任何值,也就不会为类数据成员进行初始化,并且,当且仅当没有定义任何构造函数时,编译器才会提供默认的构造函数,为类定义了构造函数后,程序员就必须为它提供默认构造函数

如果我们提供了一个非默认构造函数该构造函数接受三个参数来初始化类的成员变量。

Stock::Stock(const std::string &co, long n, double pr)
{
        company = co;
        shares == n;
        share_val = pr;
}

在这种情况下,如果我们没有提供默认构造函数,而只提供了非默认构造函数,编译器将不会为我们自动生成默认构造函数

如果我们尝试创建一个没有参数的 Stock对象,例如:

Stock obj;

由于我们没有提供默认构造函数,编译器将无法生成默认构造函数,因此会导致编译错误,编译器会提示找不到匹配的构造函数,因为它会期望找到一个没有参数的构造函数来创建对象,要解决这个问题,我们可以通过提供一个带有默认参数的构造函数来创建一个类的默认构造函数,例如,我们可以重载构造函数,添加一个没有参数的版本,或者为现有的构造函数提供默认参数,这样就可以允许创建没有参数的对象

定义默认构造函数的方法

1.给已有的构造函数的所有参数提供默认值

//写在函数声明中
Stock(const string &co = "AAA", int n = 0; double pr = 0.0);

//写在函数定义中
Stock::Stock(const std::string &co, long n, double pr)
{
        company = co;
        shares == n;
        share_val = pr;
}

此时只能在函数声明时填充值,而不能在函数定义时填充

2.通过函数重载的方式来定义另一个构造函数——一个不带参数的构造函数

//类函数声明
Stock();

//类函数定义
Stock :: Stock()
{
    company = "AAA";
    shares = 0;
    share_val = 0;
    total_val = 0;
}

 注意:只能有一个默认构造函数

析构函数

当使用构造函数创建完对象之后,程序负责跟踪该对象,直到过期为止,当对象过期之后,程序将自动调用一个函数——析构函数,析构函数将完成清理工作,例如,当构造函数使用new运算符时,析构函数将会调用delete运算符来释放申请的内存,如果不需要使用析构函数来进行清零操作,则只需要让编译器生成一个什么都不做的隐式析构函数即可

析构函数的原型

与构造函数一样,析构函数的名称也很特殊,需要在类名前加上~,并且,和构造函数一样,析构函数可以没有返回值和声明类型,与构造函数不同的是,析构函数没有参数

在这里定义了一个什么都不做的析构函数,这样定义的析构函数可以省略,因为它并没有进行任何实质性的操作,编译器会自动生成默认的析构函数

这就是一个我们自己定义的一个析构函数,但是这个函数什么都不做,里面的显示是为了让我们知道析构函数已经被调用

析构函数的调用

析构函数的调用由编译器来决定,通常不应在代码中显式的调用析构函数

如果创造的是静态存储类对象,则析构函数将在程序结束时自动被调用

如果创造的是自动存储类对象,则析构函数将在执行完代码块时(该对象是在其中定义的)自动被调用

如果对象是通过new创建的,则它驻留在栈内存或自由存储区中,当使用delete来释放时,其析构函数将自动被调用

程序可以创建临时变量来完成特定的操作,在这种情况下,程序将在结束对该对象使用时自动调用析构函数

类对象的初始化

当我们定义了一个类Stock,使用类创造对象时,可以使用两种方式进行初始化

第二条语句是初始化,它创建有指定值的对象,可能会创建临时对象也可能不会,第一条语句是赋值,在赋值语句中使用构造函数总会导致在赋值前创造一个临时变量

如果既可以通过初始化,也可以通过赋值来设置对象的值,则应该采用初始化的方式,因此通常这种方式的效率更高

C++11中,也可以使用列表初始化作用于类

const成员函数

当成员函数被声明为 const 时,意味着它不会修改对象的状态

当我们使用Stock创造对象时使用了const来进行修饰 ,即

const Stock food("bbb",10,22.1);

food.show();   //这是错误的

此时如果我们的类成员函数show()的代码无法确保调用对象不被修改,那么我们就不能通过这个const对象来调用这个类成员函数,我们要保证调用对象与const一样,不被修改,因为这个函数没有参数,因此我们不能将函数参数声明为const或使用const的指针来解决问题

C++提供的解决办法是将const关键字放在函数的括号后面,即show()的声明应该变为

void showy() const;

并且,函数定义应该为

以这种方式声明和定义的类函数称为const成员函数,就像应尽可能将const引用和指针用作函数形参一样,只有类方法不修改调用对象,就将其声明为const

this指针

为什么使用this指针

对于我们上面所定义的Stock类,每一个类成员函数都只涉及了一个对象,即调用它的对象,但是有时候方法可能会涉及到两个对象,例如我们要在我们的Stock类里新加一个成员函数,来比较两支股票谁的价格更高,那我们应该怎么解决呢?

我们可以定义一个成员函数为topval(),当函数调用cat.topval()将访问cat对象的数据,而food.topval()将访问food对象的数据,如果我们要通过这个函数比较这两个对象,就要将第二个对象作为参数传递给它,出于效率的方面考虑,我们传递引用const Stock &,并且,我们直接让方法返回一个引用const Stock &,该引用指向股票价值较高的对象

因此我们就可以得出原型:

const Stock & topval(const Stock & s) const;

这个函数隐式的访问了一个对象,显式的访问了另一个对象,并返回了其中一个对象的访问,括号中const表明,该函数不会修改被隐式的访问的对象,由于返回两个cosnt对象之一的引用,因此返回值类型也为const

假如我们要继续两个对象的比较,我们就可以使用下面两条语句

top = cat.topval(food);

top = food.topval(cat);

第一种隐式访问了cat,显式访问了food,而第二种我们隐式访问了food,显式访问了cat,无论使用哪种方式都可以,但是在实现topval()时,就会引发问题

const Stock & Stock::Stock topval(const Stock &s) const
{
    if(s.total_val > total_val)
        return s;
    else
        return ????
}

当我们显示的访问的对象较大时,我们可以直接返回显式访问的对象,否则,将会返回用来调用该方法的对象,假如我们调用的是cat.total_val(food),则s是food的引用,也是food的别名,但是,这个时候cat却没有别名,那么我们该如何去称呼它呢?

this指针的用法

C++解决这种问题的方法就是使用this指针

this指针指向用来调用成员函数的对象(this被作为隐藏参数传递给方法),这样,函数调用

cat.topval(food)时将this设置为cat对象的地址,使用这个指针也可以使用topval()方法,一般来说,所有的类方法都将this指针设置为调用它的对象的地址,我们可以使用->运算符,通过指针来访问我们的结构成员,这在类里也适用

注意:每个成员函数(包括构造函数和析构函数)都有一个this指针,this指针指向调用对象,如果方法需要引用整个调用对象,则可以使用表达式*this,在函数括号后面使用const将this指针限定为const,此时不能使用this指针来修改对象的值

然而,我们需要返回的不是this,因为this是对象的地址,我们要返回的是对象的本身,因此,我们可以在this指针前加上*this,这样,我们就可以将*this作为调用对象的别名来完成前面的方法定义

const Stock & Stock::Stock topval(const Stock &s) const
{
    if(s.total_val > total_val)
        return s;
    else
        return *this;
}

在这段代码中,total_val应该是this指针的total_val, 即 this->total_val;

对象数组

用户有时候会创建同一个类的多个对象,我们可以使用创建多个独立对象变量,也可以使用创建对象数组的方式,声明对象数组的方法与声明标准类型数组的方式一样

Stock cat[10];

这就是一个对象数组,数组名为cat,创造了10个Stock类的对象

如果我们程序岁这样写的,那么我们并没有进行显式的初始化类对象,而是调用的默认构造函数,我们也可以使用构造函数来初始化数组元素,在这种情况下,我们必须为每个元素调用构造函数

Stock cat[4] = {
                Stock("AAA",12.3,10),
                Stock("BBB",13.2,20),
                Stock("CCC",11.3,50),
                Stock("DDD",13.6,50),
                };

这里的代码使用了标准格式对数组进行了初始化,用括号括起来,用都会分隔的值的列表,其中,每次构造函数调用都表示一个值,如果类里面包含了多个构造函数,那么我们就可以对不同的元素使用不同的构造函数

Stock cat[4] = {
                Stock("AAA",12.3,10),
                Stock(),
                Stock("CCC",11.3,50),
                };

这里只初始化了其中的三个,剩余的会使用默认的构造函数进行初始化

类作用域

类作用域说明

在类中定义的名称的作用域都为整个类,作用域为整个类的名称只在该类里面是已知的,在类外是不可知的,因此,可以在不同的类中使用相同的类成员名而不会引起冲突,例如我们定义了Stock类和Person类,那么这两个类里都有shares成员,则Stock中的shares成员与Person类中的shares成员是不同的,此外,类作用域意味着不能从外部直接访问类的成员,公有函数也是这样,即我们要调用公有函数,只能通过类的对象

在这个类的定义中,我们只能通过类的对象来调用成员函数

Stock cat;

cat.show();

这是允许的,在创建cat对象时使用了默认构造函数

show();

这是不被允许的,是无效的

总之,我们可以在类声明或者成员函数定义中,我们可以使用未修饰的成员名称(未限定的名称),就像我们上面代码中sell()调用set_tot()成员函数时那样

构造函数名称在被调用时,才能被识别,因为它的名称与类名相同,在其他情况下,必须根据上线代码使用直接成员运算符(.)或者间接成员运算符(->)或作用域解析运算符(::)

作用域为类的常量

有时候,使符号常量的作用域为类很有用,例如,类声明使用一个常量来指定数组的长度,由于该常量对于所有的对象来说都是相同的,因此可以创建一个有所有对象共享的常量,例如

class A

{

        const int MONTHS = 12;

        double cost[MONTHS];

        public:

                ......

}

但是这样是行不通的,因为声明类只是描述了对象的形式,并没有创建对象,因此,在创建对象之前,没有开辟内存空间,因此将没有用于存储值的空间

但是,我们有两种方式来进行解决

1.我们可以在类中声明一个枚举类型,在类的声明中,声明的枚举的作用域是整个类,因此可以为整型常量提供作用域为整个类的符号名称

class A

{

        enum{const int MONTHS = 12};

        double cost[MONTHS];

        public:

                ......

}

注意,此时用这种方式声明并不会创建类数据成员,即所有的对象中都不包含枚举,MONTHS只是一个符合名称,在作用域为整个类的代码中遇到它,编译器会使用12代替,由于这里的枚举类型只是为了创建符号常量,并没有真正的创建变量,因此不需要提供枚举名

2.使用static关键字

class A

{

        static const int MONTHS = 12;

        double cost[MONTHS];

        public:

                ......

}

这里会创建一个静态的常量,该常量与其他的静态变量存储在一起,而不是存储在对象中,这里常量在编译时就已经开辟了内存空间,即使不调用,它也会存在

抽象数据类型

对于我们上面定义的Stock类来说,它是非常具体的,然而,程序员通常会定义类来表示更为通用的概念,例如对于抽象数据类型而言,使用类是一种非常好的方式,抽象数据类型(ADT)以通用的方式来描述数据的类型,而没有引入语言或者实现的细节,我们以栈为例来看

栈是一个存储多个数据项的容器,栈的操作如下

1.可以创建空栈

2.可以将数据项用栈顶入栈

3.可以从栈顶删除数据项

4.可以查看栈是否为满

5.可以查看栈是否为空

我们就可以将这些描述转换为一个类的声明,其中公有成员函数表示栈的操作,而私有成员负责存储栈的数据,类的概念非常适合抽象数据思想

  • 40
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值