首页 > 技术文章 > C++Primer学习——类

Przz 2017-02-19 15:21 原文

我们在创建类的对象时,类不应该仅仅被声明,还应该被定义过,否则无法知道类占用了多少的内存
但是如果一个类的名字已经出现过就被认为是已经声明过了,所以允许包含自己的指针或者引用。

默认构造函数:

当类中包含一个其他类的成员且它没有默认构造函数,那个编译器无法为当前类合成默认构造函数。
如果不支持内类初始值,那么所有构造函数都应显式的初始化每个内置类型成员

使用vector或string能避免分配、释放内存带来的复杂性
struct和class唯一的区别就是默认访问权限,struct(public) class(private)

初始化列表:

对象成员的初始化发生在进入构造函数本体之前,构造函数内的只能算是赋值
如果没有初始值列表显示初始化成员,会在构造函数之前执行默认初始化
(如果想创建一个常量对象,构造函数完成初始化过程,对象才真正有常量属性)。

class fo
{
public:
  fo(int i);
private:
  int ai;
  const int ci;
  int &ri;
}
fo::fo(int i)
{
   ai = i;
   ci = i;  //error  不能给const赋值
   ri = i;  //error  ri未初始化
}
fo::fo(int i):ai(i),ci(i),ri(i){}   //correct

//如果成员是const,引用或者某需要提供默认初始值的类类型,要通过构造函数初始值列表提供。
如果构造函数没有初始值列表,那么我构造函数之前会先进行默认初始化

委托构造函数:

class sales
{
public:
   sales(string s,int cnt):bookNo(s),units_sold(cnt){}   //1
   
   sales():sales("",0,0) {}                              //2  利用1
   sales(string s):sales(s,0,0){}                        //3  利用1
   sales(iostream &is):sales(){}                         //4  利用2
}

数据成员:

类内部的成员函数和友元函数是隐式内联的。

const成员函数(修改隐式this指针的类型):
所以一个成员函数被标记为const,则它不能调用一个非const的成员函数(隐式this指针,无法从const转变成非const)

class aa{
    int num;
public:
    void out2() const{
        cout<<num<<endl;
    }
    void out3() const{
        num+=10; //error,const函数不能修改其数据成员
        cout<<num<<endl;
    }
};

定义在类内部的函数是隐式inline.结构体内部this指向自己,一个常量指针.
但在const成员函数中,this会被修改成 指向常量的常量指针,所以无法修改值
编译器会首先编译成员声明,然后才是成员函数

友元

有的函数是类接口的一部分,但又不是类的成员。友元可以允许其它类或者函数访问它的非公有成员。
友元只能声明在类内部,不是类的成员所以不受它所在区域的访问控制级别的约束。
不具有传递性、不能被继承、单向无交换性

令成员函数作为友元,注意作用域:
先定义man,声明disp不定义。在disp使用woman前声明它。
定义woman然后disp的友元声明。
定义disp

//成员函数的friend声明必需在定义之前,需要用到类限定符。所以man必须先被定义
class woman; // 前向声明  
class man  
{  
public:  
    void disp(woman &w);  
};  
class woman  
{  
public:  
    friend void man::disp(woman &w); // 将man的其中一个成员函数disp()设为woman的友元函数,就可以使用该函数访问woman对象的私有成员了  
  
private:  
    string name;  
};  
void man::disp(woman &w)  
{  
    cout << w.name << endl;  
}  

类和非成员函数的声明不是必须在它们的友元声明之前。

struct X
{
  friend void f() {]
  X(){ f(); }
  void g();
  void h();
}
void X::g(){ return f(); }
void f();
void X::h(){ return f(); }

如果想把一组重载声明为友元,必需对戏中的每一个分别声明

可变数据成员:

有时候希望改变const对象里面的某个数据成员,可通过在数据成员加上mutable实现。
而一个可变的数据成永远不可能是const
mutable size_t acces;

返回*this的成员函数:

返回引用和非引用的区别

class Screen
{
public:
    Screen &set(char);
    Screen &move(int ,int);
};
inline Screen& Screen::set(char c){}
inline Screen& Screen::move(int x,int y){}
int main()
{
    Screen myScreen;
    myScreen.move(4,0).set('#');
    
    myScreen.move(4,0);  //如果返回引用,等效于此
    myScreen.set('#');
    
    Screen temp = myScreen.move(4,0);     //如果返回非引用。。,则是一个副本
    temp.set('#');
}

const成员返回*this时,不能嵌入一个动作序列
如果display返回常量引用,调用set则会产生错误。。无法修改一个常量对象

基于const的重载:

如果某个对象上面调用display,该对象是否是const决定了调用display的那个版本

Class A {
int display();
int display() const;
};

原因是:按照函数重载的定义,函数名相同而形参表有本质不同的函数称为重载。在类中,由于隐含的this形参的存在,const版本的 display函数使得作为形参的this指针的类型变为指向const对象的指针

不完全类型:
一旦一个类名出现,就被认为是声明过了(但尚未定义),因此允许包含自己的指针。
在声明之后,定义之前是一个不完全类型。
①可以定义指向这个的指针和引用 ②也可以声明以不完全类型作为参数或者返回类型的函数

class Link_screen
{
  Screen windows;
  Link_screen *next;
}

作用域:

一旦遇到类名,那么剩下的部分就在类的作用域之内了(参数列表和函数体)

class Window_mgr
{
public:
   ScreenIndex addScreen(const Screen&);
};
Window_mgr::ScreenIndex
Window_mgr::addScreen(const Screen& a)
{}

对于类成员函数的名字查找:
1.编译成员的声明 2.直到类全部可见之后才编译函数体

class Account         //balance会在整个类可见之后才被处理
{
public:
   int balance(){return val;}
private:
   int val;
}

类型名:

通常内层作用域可以重新定义作用域外的名字,即便这个已经被用过。
但是在类中,如果使用外层作用域的某个名字,而这名字代表一种类型,则不能重新定义

typedef double mon;
class Account{
public:
   mon balance(){};
private:
   typedef double mon;        //不能重新定义mon
}

成员定义的普通块作用域的名字查找:
1.先成员函数内部 2.类内全面查找 3.成员函数定义之前的作用域
所以此例中生效的是pos height,但是它隐藏了同名的成员,对于被隐藏对象可以通过作用域符 ::

int height;
class Screen{
public:
  typedef std::string::size_type pos;
  void dup(pos height)
  {
     cursor = width*height;              //width*this->height,不建议隐藏同名
  }
private:
   pos height = 0,width = 0;
   pos curson;
}

类类型转换:

隐式转换:
能通过一个参数的拷贝构造函数定义一条从构造函数参数类型到类类型的隐式转换规则。
(只有一个实参的能用于隐式转换)

struct foo
{
    foo(){}
    foo(int a):real(a){}
   // int b = 2;
     const foo& operator+(foo c){
        return foo(this->real + c.real);
    }
    int real;
};

int main()
{
    foo f;
    f = 10;           //会生成一个临时的foo
    cout <<f.real <<endl;
    f = f + 100;
    cout << f.real <<endl;
}

只允许一步转换:

string->foo   correct
"9999"->string->foo   error
item.combine("9999");  //error
item.combine(string("9999"));  //correct 显式转换string,隐式转换 
item.combine(sales("9999"));  //correct 隐式转换string,显式转换sales

我们可以通过将构造函数声明为explicit阻止隐式转换,函数只能用于直接初始化
(只类内部的声明时使用,在类外定义时不需要重复explicit)

struct node
{
    explicit node(){}
    explicit node(string s){}
    node& combine(node &b){}
};
int main()
{
    string s = "xxx";
	node a("xxx");
	node b(s);
	//node a = s;       //error 没法隐式的创建对象node
	//a.combine(s);     //error
}

静态成员:

static将其与类绑定起来。在类外定义时不能重复static.一般来说不能在类内定义初始化静态成员(可能产生重复定义)
如果静态成员是constexpr类型并且初始值是常量表达式,可以在类内进行定义。

而且由于静态成员函数不与对象绑定,不包含this指针,所以也不能被声明为const
因为静态成员函数属于整个类,在类实例化对象之前就已经分配空间了,而类的非静态成员必须在类实例化对象后才有内存空间,所以这个调用就出错了

class Point
{
public:
    void init()
    {
    }
    static void output()
    {
        printf("%d\n", m_x);           //error
    }
private:
    int m_x;
};
int main()
{
    Point pt;
    pt.output();
}

static数据成员,不可被定义在inline函数中.inline函数在编译时会被编译为2进制代码并重复嵌入各函数体中,而static类型数据成员只可被初始化一次,inline函数中使用static的话,将会造成static类型数据成员被多次初始化错误.

静态成员可以是不完全类型,静态成员可以作为默认实参

class Screen
{
public:
  Screen &clear(char = back);
private:
  static const char back;
};

推荐阅读