C++中的类初探

类的三大特性

封装:安全性
继承
多态

1.关于类的基本概念

1.1基本思想

类的基本思想是数据抽象和封装,抽象一种依赖于接口和实现分离的技术,类的接口包括用户所能执行的操作。封装是指类隐藏其内部实现细节。
也就是说用户只能访问接口。

1.2成员

  • 成员变量:数据抽象部分
  • 成员函数:功能实现
    在C++程序的编译过程中,首先是进行成员变量的声明的编译,然后才是成员函数体的编译,因此成员函数可以随意使用成员变量,无论它在类内的何处声明。

1.3类的基本特性

1.3.1 访问控制与封装

一般说的访问控制是通过访问说明符对类内部的成员的访问权限做一个限制,C++中规定了 privatepublic两种访问说明符。

  • public:表示成员在整个程序内可访问,一般用来定义类的接口
  • private:表示成员只能被类内的成员函数访问,private一般用来隐藏类的实现细节。
    封装:是指保护类的成员不被随意访问的能力;一般通过上述的访问控制实现类的封装,类实现封装有以下优点:
    1. 确保用户代码不会被无意间破坏
    2. 被封装的类的内部具体细节可以随时改变,此时无需改变用户级别代码。

1.3.2 友元函数

其实对于类来说并不一定绝对的不允许非成员函数对其private变量的访问,为了缓和这种设计,C++中提供了friend关键字修饰的友元函数,此类函数的用法很简单,只需要在其本身的函数声明之前加一个friend就可以。需要注意的是友元函数的声明只能出现在类定义的内部
前面所说的类内部的声明只是说明了友元函数的访问权限,并不是一个普遍意义的声明,因此如果我们需要用户能够调用友元函数,那就需要在友元声明之外再做一次函数声明。如下所示。不同的是其在cpp文件里的实现可以像普通的类外函数一样不用再加类名。

class Person{
    friend std::istream &read(std::istream&, Person&);
private:
    std::string name;
    std::string address;

public:
    Person()=default;
    Person(const std::string &na):name(na){}
    Person(const std::string &na,const std::string &ad):name(na),address(ad){}
};
std::istream &read(std::istream&, Person&);
//实现部分
std::istream 
&read(std::istream& in, Person& per){

}

2.类的成员

2.1 返回*this的成员函数

2.1.1 返回this指针的解析

我们先来再了解一下this指针,this指针并不是类对象本身的一部分,this的作用域也只是在类内部。换句话说就是当类的非静态成员函数中访问类的非静态成员的时候编译器会自动将类对象本身的地址作为一个隐含参数传递给函数,也就是说即使我们没有声明使用this指针,编译器也会加上一个this指针。例如调用1.3.2中的函数Person(const std::string &na)时,则在编译器看到的是**Person(Person *const this,const std::string &na)**。
一般我们使用this绑定返回值,也就是返回调用该函数本身的对象。看下面一个函数

SaleItem& 
SaleItem::combine(const SaleItem &rhs){
    unitSold += rhs.unitSold;
    revenue += rhs.revenue;
    return *this;
}

如果我们声明一个SaleItem对象名为saleItem,并作调用saleItem.combine(rSale);则在调用该函数时saleItem的地址被绑定到隐式的this参数上,最后的return语句解引用this指针以获得原本的对象。

2.1.2 返回const成员函数的*this

此时的this指针是一个指向常量值的常量指针,也就是有const Person *const this此类的定义。

2.2 友元

2.2.1 友元函数

这个在前面已经说过就是类外的函数可以访问类内的私有属性部分;需要补充的是我们还可以把其他类内的成员函数声明为该类的友元函数。

2.2.2 友元类

1)类之间的友元关系。当一个类A把其它的类B(之前定义过的)声明为友元类,那么类B的成员函数就可以访问类A非公有成员之内的所有成员。声明方式如下:

class A{
    friend class B;

2)此外还可以单独将类B的某个成员函数声明为类A的友元函数

class A{
    friend void B::function();
这样的话需要遵循以下原则:
	1.首先定义B类并在其中声明function函数,但是不能对其进行直接的定义,在B的function函数中使用类A的成员之前必须先声明A
	2.然后定义类A,加上其对友元函数的声明
	3.最后定义function函数

2.3 类的作用域

2.3.1 作用域理解

首先明白一个概念:一个类就是一个作用域;
也就是说类内相当于一个局部作用域,如果我们在其中定义了一个变量要在类外使用必须进行作用域说明。如

class A{
using pos  = vector<int>::size_type;
pos f();
}

就应该使用下面的定义语句

A::pos A::f(){

前面已经看到过,类的作用域通过符号::来进行界定。前面我们将生命在类内的函数定义在类外的时候,通常在其函数名前有一个类名,我们在这里对其作一个解释,这是因为一个类就是一个作用域,当在类外定义时必须指明该函数是哪个类的成员。

2.3.1 作用域与名字查找

1)名字查找就是寻找与所用名字最匹配的声明的过程。遵循以下前后步骤:

  1. 首先现在名字所在的块内查找,这时只考虑名字使用之前出现的声明;
  2. 如果没找到则继续在外层作用域查找
  3. 最终未找到则程序报错

2)一般来说名字查找虽然遵循以上步骤但是也可分为多种情况下的查找:

  • 类成员声明的名字查找:类内(待查找的名字使用之前)–>类外
  • 类型名的确定:内层作用域和外层作用域名字冲突
  • 成员函数中使用的名字:类外函数使用之前出现的声明–>类内所有的范围进行查找–>外层作用域

良好的设计最好还是不要重名
看下面一个例子:

typedef std::string Type;   //声明类型Type表示string
Type initVal();             //声明函数initVal,返回类型为Type(string)
class Exercise{     
public:
    typedef double Type;       //内层作用域声明类型Type表示double
    Type setVal(Type);          //double setVal(double);
    Type initVal();             //double(initVal);
private:
    int val;
};
// 定义函数setVal,此时第一个Type表示的是string,第二个Type是double
//Type Exercise::setVal(Exercise::Type)
//与Exercise::Type Exercise::setVal(Exercise::Type)不兼容
//修改为Exercise::Type Exercise::setVal(Type)就会编译正确
~~Type Exercise::setVal(Type parm){~~    
	//initVal 使用的是类内的initVal
    val+=parm +initVal();
    return val;
}

2.4 构造函数

构造函数的作用:成员变量的初始化,只要类被初始化就会执行构造函数;
构造函数的形式:没有返回类型,参数列表可能为空,函数体也可能为空。构造函数不能被声明为const。一般比较多使用的是以下形式

	ClassX(int val):i(val),j(val){}
    ClassX(int val,int ival):i(val),j(ival){}

其中ClassX是类型,里面两个是其两个构造函数。该函数定义比较新奇的是出现的冒号已经冒号和花括号中间的部分代码。其中{}内部的是函数体(可能为空),冒号与“{”之间的是构造函数初始值列表,它负责为新创建的对象的相关数据成员进行赋值。

一般由于构造函数的主要作用是为成员变量赋值,所以其函数体为空,一旦有其他任务需要执行就不会为空了。

2.4.1 成员变量初始化

1)初始化和赋值
初始化和赋值事关底层效率,初始化是直接初始化数据成员,赋值时先初始化再赋值。而且再一些情况下。数据成员必须被初始化,比如数据成员为常量、引用等等。
2)成员变量初始化顺序
成员的初始化并不是杂乱无序的,所有成员变量的初始化顺序和他们在类中出现的先后次序一致,从第一个到最后一个。看下面的例子。虽然里面的构造函数 X(int val):j(val),i(j)的初始化列表是j在前,但是其初始化顺序是根据在类中出现的先后次序来的,所以是先初始化i然后初始化j,所以该代码是会报错的。

class X{
    int i;
    int j ;
    X(int val):j(val),i(j){}
    X(std::istream &is = std::cin){is>>i>>j;};
};

良好的代码风格应该在我们写构造函数初始化列表时最好令初始值顺序和成员声明的顺序保持一致
3)默认实参和构造函数
一般来说我们提倡提供给定默认实参。设想一种情况,类的一个构造函数形参都提供默认实参,此时这个构造函数就实际上也定义了类的默认构造函数。但是一些情况下给定过多的默认实参可能会出现二义性错误。看下面一个例子

class X{
    int i;
    int j ;
    X(int val = 0):i(val),j(val){}
    X(std::istream &is = std::cin){is>>i>>j;};
};

虽然看起来这个类的设计是没有问题的,但是实际上它包含了两个默认构造函数,一旦我们不提供实参创建对象就会出现二义性错误

2.4.2 委托构造函数

名字起的高大上,其实就是一个构造函数找其它构造函数替他干活;如以下形式。其中第二个构造函数就是委托第一个构造函数替它进行初始化。注意:委托时受委托的函数全部执行完才会将控制权交还给委托者函数。根据下面的运行结果可以得出委托函数执行顺序还是根据原构造函数的顺序来的,也就是说先进行成员变量初始化—>(委托转向另一个构造函数)—>函数体执行

	X(int val):i(val),j(val){cout<<"我是受委托函数;";}
    X():X(0){cout<<"我是委托者函数;";}
    //运行结果是:
    //我是受委托函数;我是委托者函数;

2.4.3 默认构造函数作用

这一点和java有一些区别,在C++中,如果我们自己定义了非默认构造函数的构造函数,那么此时类就没有默认构造函数里,虽然可以编译运行,但是这是十分不好的。所以建议如果定义了其他的构造函数就最好也为类提供一个默认构造函数。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值