C++学习之路(9)---C++面向对象的总结

来源:互联网 发布:淘宝网书城首页 编辑:程序博客网 时间:2024/05/21 10:02
访问权限:public,protected,private。
访问权限主要针对代码范围,如果是public则任何代码都可以访问;
protected只有类成员函数及子类的成员函数或友元的代码(当然也是通过对象,指针,引用访问或类域(静态数据成员)),
   注意子类中的对protected成员的访问不能通过基类对象,引用或指针,只能通过派生类的对象,引用或指针。
private只能类成员函数或友元代码访问。(当然也是通过对象,指针,引用访问或类域(静态数据成员)),




const int *ptr 或 int const *ptr 指向const类型的指针
int *const ptr  const 指针
const int *const ptr 指向const对象的const指针


注意:
typedef string *pstring
const pstring cstr;
const 修饰pstring,表示const指针,所以等价于string *const cstr;


引用必须初始化,函数参数除外,作为函数参数时,可以传递对象,作为返回值时,可以返回对象。


const int & const引用,可以绑定不同但相关类型的对象或右值,const对象和非const对象都可,不能通过其修改绑定的对象。
非const引用只能绑定非const的该引用同类型的对象。


class A;
前向声明,只能用来定义指针或引用,或声明(不是定义)函数。
类的定义遇到}就结束,此时就知道这个类所需的存储空间,不必预先定义完成员函数,此时类就可以用来定义任何类型。


this指针,指向自身对象,在const函数中不能通过this指针改变对象的成员,除被声明为mutable,在const时如果要返回
对象的引用,必须是const &。


基于const的重载,const对象只能调用const函数,非const对象都可,但非const函数是更好的匹配。


类作用域,类的成员函数除返回类型外,其他都在类的作用域类,名字查找,有下至上。


构造函数初始化式
A():M1(),M2(m2){} 只能在构造函数定义处
必须对const或引用类型成员以及没有默认构造函数的类类型的任何成员使用初始化式。
可以在初始化式中调用成员的某一构造函数。


构造函数中没有显示初始化的,类类型调用默认构造函数,(默认构造函数:无参数或有默认实参的构造函数);
内置和复合类型,如果在全局于中定义对象,初始化为0,否则为未定义。
只有当类没有定义构造函数时,编译器才会合成一个默认构造函数,每一个类应该定义一个默认的构造函数。


使用explicit修饰构造函数,抑制隐式转换。
隐式转换:比如类A具有一个构造函数,参数是string,如果有个函数f以A作为参数,若调用
f(stirng),就会发生隐式转换,string类型转换为A。


友元:可以访问类的私有成员,当然也是通过对象访问。
声明一个类或非成员函数为友元时,类与函数不必预先声明,但声明一个成员函数时必须定义类,就是class ... {};




引用static数据成员,类域符,对象,引用,指针都可引用,也属于静态数据,与特定类相关。
静态非静态成员函数都可访问static数据成员。


static 数据成员
在类体中声明,在类体外定义并初始化,static
关键字只能用于类中声明,不能用于定义。
定义格式:double A::B=初始化式;
如果类中有多个static数据成员,static数据成员初始化的次序是
按照static数据成员在类中的声明次序进行初始化的
访问权限与普通成员一样。




static const int 可以在类体内定义并初始化。


static 成员函数
也只需在类体标明static,没有this指针。
只能使用static成员,不能声明为const,要想访问非static成员,可以传进一个对象。也可以通过对象,指针,引用访问它们,与普通成员一样。


static只对修饰的函数或成员起限制,访问它们与普通成员无区别,也是遵循一般的访问规则(public,protected,private)。


复制构造函数


A(const &A)
{
}
通常有指针成员的时候要自己定义复制构造函数。
未定义,系统会合成。也是一种构造函数,与构造函数具有相同的特点。
即在复制构造函数中没有显示初始化的,类类型调用默认构造函数,(默认构造函数:无参数或有默认实参的构造函数);
内置和复合类型,如果在全局于中定义对象,初始化为0,否则为未定义。




重载赋值操作符:
A & A:: operator=(const A &B)
{
}


析构函数:
生存期结束或调用delete会调用析构函数,撤销非static成员。
即使自己定义了析构函数,也会调用合成析构函数,构造函数和重载运算符就没有。
 
 


智能指针:
如果类有个指针成员,应使用计数类来定义这个指针成员,并且要重写析构,复制构造,重载赋值操作符。


计数类,有点类似于window下的内核对象。
class U
{
  friend class Has_U; 
  U():a(0.0),use(1){}
  double a;//这个对象的值
  int use;//引用计数
}


class Has_U
{
   int cc;
   U *x;//必须要初始化指向一个计数类
   Has_U():cc(0)()
   {
       x=new U();
   }


   ~Has_U(){
     if(--x->use==0)
   delete x;
   }   
   
   Has_U(const Has_u &ret)//产生一个新的对象
   {
     ret->use++;
   }
   Has_U & operator=(const Has_u &ret)//只是改变一个对象
   {
       ret->x->use+;
  if(--x->use==0)
    delete x;
cc=ret.xx;
      return *this;
   }
 }


1.创建一个新的对象,即调用构造函数时,初始计数类的引用计数为1,析构默认或其他清理,比如计数类的值也有可能是个指针。
2.在拥有这个计数类对象的指针的类中,构造函数常规,复制构造函数一定创造了一个新的的对象,可以让其指向的计数类的引用
计数+1,赋值操作符,右操作数指向的计数类的引用计数+1,左操作数的引用计数-1,析构函数将当前对象指向的计数类对象的引用计数-1。
每次将计数类引用-1,都要判断是否为0,为0就将计数类删除。






运算符重载:
输入输出必须定义为非成员函数,然后类把其声明为友元
ostream& operaror<<(ostream &out,const A &ret)
{
     out<<...
}


istream&operator>>(istream &in,A &ret)
{
    //必须处理输入错误及文件结束
in>>...
if(in)
 {
    输入成功,进行后续处理
 }
else  
      {
    输入错误,进行相应的处理
 }
}




算术操作符,返回一个右值,非成员。


A operator+(const A &a,const A &b)
{
     A tmp;
...
return tmp;
}




bool operator==(const A &a,const A &b)
{
  


}




A &operator=(const A &ret)
{
  if(this!=&ret)
  {
    防止自身赋值
  }
  ...
  return *this;//返回左操作数的引用
}




下标操作符要重载两个


int &operator[](const size_t)


const int &operator[](const size_t)


解引用重载×


A &operator*()
const A &operator*()


重载箭头操作符,一元操作符,操作数在左边。


class A
{
    B *operator->()
{
 
}
const B *operator->()const
{
    
   
}
}


A a;
a->display(argc);
等价于(a->display)(argc)
等价于(a.operator->()->display)(argc)


自增


class A
{
   int cur;
   A &operator++()
   {
      ++cur;
 return *this;
   }//前缀操作符
   A operator++(int)
   {
     A B(*this);
++(*this);
return B;
   }//后缀操作符,有个没用的参数int。
   
   //注意返回值不一样,一个是当前应用,另一个是之前的副本。
}


重载调用符


class Abs
{
  int operator()(int val)
  {
    return val<0?-val:val;
  }
}


这个类也叫函数对象






继承与多态


继承
class A:public B
{
  


}
B要预先定义,声明派生类class A:public B;


基类的指针可以指向派生类对象
友元关系无法继承。


虚函数:
基类中用virtual说明,派生类中如果要重写,必须跟基类的函数原型一致,如果基类的虚函数的返回类型是基类的指针或引用,
派生类可以返回对应的派生类的指针或引用。只要基类声明是虚函数,它就一直是虚函数。
虚函数的默认实参是在编译的时候就确定,不是运行的时候确定,如果调用虚函数,使用默认实参要注意。
一般重写虚函数时要显示调用基类的虚函数,完成共同部分的初始化,在调用初始化派生类自己的成员。
虚函数机制可以用类作用符覆盖。


对于数据成员,派生类对象包含了基类的全部成员,无论是什么继承,子类的成员函数无法访问基类private成员,但可以访问其他成员,
但不同的继承会影响基类成员在子类的访问标号进而影响用户区代码对子类成员的访问,及子类的子类的对子类成员的访问,静态成员的继承与
普通成员一样。
对于非虚成员函数也是如此,如果虚函数没有重新定义,也是如此,虚函数重新定义可以理解为重写了基类的成员函数。


真实使用的时候不用关心派生类实际包含哪些成员,利用从派生类到基类的名字查找即可,虚函数注意一下就行。






访问权限:
不管怎么继承,基类的private成员子类都不可访问,不管怎么继承,子类中的成员函数都可访问基类的protected和public成员。
不同的继承改变的是基类成员在子类的访问标号,影响的是在子类的用户去访问或子类的子类访问。


默认是public继承


对于pubilc继承,基类成员的访问级别在子类不变。
对于protected继承:基类中public和private成语啊变成了子类的protected成员
对于private继承:基类的所有都变成了子类的private成员。






派生类到基类的自动转换


可以将基类指针或引用指向派生类对象。
可以用派生类对象对基类对象进行赋值初始化。


派生类到基类的可访问性,如果代码可以访问到基类的public成员,转换就是可访问的,否则不可访问。


基类到派生类的自动转换不存在,要强制转换。




静态类型:编译时确定的,就是定义的类型。
动态类型:主要针对指针和引用,指向的对象的类型到运行的时候才能确定。


继承的作用域


派生类的作用域嵌套在基类中。
也就是说名字查找先找派生类,再查找基类...,在编译时发生。
如一个派生类定义了一个新成员,有个基类指针指向一个派生类对象,此时若通过基类指针访问新成员,会出错,因为
名字查找在编译时确定,虽然它指向派生类对象,但这只有运行才能确定。


派生类数据成员与基类同名时,会屏蔽对基类数据成员的访问,可以通过类操作符解除屏蔽。
对于成员函数(包括虚函数),即使函数原型不一样(如返回值,参数个数等),只要函数名一样,也会屏蔽基类成员。
派生类中的函数不重载基类的函数,局部域中的函数也不重载全局作用域中的函数。




函数调用步骤:(先名字查找,然后类型匹配,然后再看是否虚函数,再动态调用)
(1)首先确定调用函数的对象,指针,引用的静态类型。
(2)根据静态类型的类查找名字,找不到就往基类找,直到找到名字或没有基类了就返回(此时没有进行参数类型个数的检查),找不到就出错。
(3)找到名字后,根据名字进行常规的类型检查(参数个数类型等),看调用是否合法,不合法出错。
(4)调用合法。如果函数是虚函数且通过引用或指针调用,则根据动态类型运行相应的版本,如果动态类型没有重新定义虚函数,就调用基类的版本,
以此类推...,否则根据静态类型运行相应的版本。




派生类的构造函数


class A
{
 A();
 A(int);
}


class B:public A
{
   合成的默认构造函数
   1.先调用基类的构造函数
   2.再初始化派生类新定义的成员。
   
   定义默认构造函数
   B():...{};
   先调用基类的默认构造函数,再初始化自己的成员。
   
   如果要调用基类指定的构造函数
   B():A(4)...{}
   
    
   赋值构造函数
   B::B(const B &ret):A::A(ret)...{};
   调用基类的复制构造函数初始化基类部分
   
   
   构造函数中也可以使用默认实参。
   构造函数只能直接初始化自己的直接基类。
   派生类中直接初始化部分只能初始化派生类自己定义的数据成员。
   
}


派生类赋值操作符:


B& B:: operator=(const B &ret)
{
   if(this!=&ret)
   {
    A::operator=(ret);
   ...
   }
   return *this;


}


派生类的析构函数
派生析构函数撤销派生类自己的成员,然后编译器会调用基类的析构函数,撤销基类的成员。
首先运行派生类的析构函数,再运行基类的析构函数。


虚析构函数
delete ptr;
将析构函数定义会虚函数的时候delete一个基类指针时,就会根据
动态类型调用相应的析构函数,否则只能根据静态类型调用析构函数,
如一个prt指向派生类对象,delete ptr就只能撤销基类部分。


所以基类的析构函数要定义为虚,即使是空的。






纯虚函数
virtual <类型><函数名>(<参数表>)=0;
包含纯虚函数的类不能定义对象。






异常处理
try
{
   throw 表达式 //可以是任意类型的对象,
                //根据这个表达式的类型与最近的catch匹配(栈展开,从局部到全局,再沿着调用链返回)
//throw会生成一个异常对象,以表达式为副本初始化,并保留到异常处理完毕,这个异常对象可以是任意类型的。
//表达式的静态类型决定抛出的异常类型
//异常可以是任意类型
   不执行
}
//接受类型匹配的异常对象。
catch(类型)//可以是任意类型的参数
{




}


所以抛出一个指针或一个指针的解引用要注意。
抛出一个指针,catch处理的时候只能保证指针值不变,但指针指向的对象可能被释放。
对于抛出一个指针的解引用,可能指针是基类型,指向的是派生类,而静态类型决定
异常类型,所以匹配的catch的参数类型就是基类类型,就导致抛出派生类对象的只有基类
部分。


try
{
  throw 100;


}
//传给i的对象在异常处理完毕后才会释放。
catch (int &i)
{
  
}
类型的匹配允许非const到const的转换,也就是throw一个非const对象可以匹配以catch同类型的const对象
允许派生类到基类的转换,允许数组到指针的转换,函数到函数指针的转换,不允许标准的算术转换,或其他的一些转换。
尽量让catch的参数作为引用或指针,便于动态绑定,否则如果一个基类型的对象接受一个派生类的对象,对象的派生
类成员就会消失。


catch语句排序时注意,拥有派生类对象参数catch最好要出现在拥有基类对象参数的catch前面,即由最低层次到最高
层次。
catch(...)捕获所有异常


catch中调用throw;重新抛出异常,
异常的类型就是原来的异常的动态类型,注意跟try抛出的时候不一样。
catch可以改变异常对象,只有catch的参数是引用的时候改变才有效,不然改变的只是异常对象的副本。


C++中还定义了一些标准异常类,这些标准异常可以throw抛出,也可以代码自己引发。

















阅读全文
0 0