C++ class

2023-05-16

namespace

在变量或函数前面加上命名空间,用来区分其它位置中的同名函数或变量。

#include <iostream>
using namespace std;
namespace A
{
    int a = 100;
    namespace B            //嵌套一个命名空间B
    {
        int a =20;
    }
}
int a = 200;//定义一个全局变量
int main(int argc, char *argv[])
{
    cout <<"A::a ="<< A::a << endl;
    cout <<"A::B::a ="<<A::B::a << endl;
    cout <<"a ="<<a << endl;
    cout <<"::a ="<<::a << endl;
    int a = 30;
    cout <<"a ="<<a << endl;
    cout <<"::a ="<<::a << endl;
    return 0;
}

class 和 stuct 区别

  • struct更适合看成是一个数据结构的实现体,class更适合看成是一个对象的实现体。

  • struct没有继承,没有封装,要说封装只有初步封装。而class把数据,接口可以以三种类型封装,private,public,protected;还可以继承和派生。

  • 它们都可以提供自己的接口函数,构造函数。一个类可以由结构继承而来。struct只能叫做数据的集合,外部可以任意访问,但是类就完成了封装,维护了数据安全,这就是面向对象的理念。

  • class里可以定义私有成员和保护成员而结构体里所有的成员,均为公用成员

从上面的区别,可以看出,struct更适合看成是一个数据结构的实现体,class更适合看成是一个对象的实现体。

inline

引入 inline 关键字的原因
在 c/c++ 中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了 inline 修饰符,表示为内联函数。
栈空间就是指放置程序的局部数据(也就是函数内数据)的内存空间。
在系统下,栈空间是有限的,假如频繁大量的使用就会造成因栈空间不足而导致程序出错的问题,如,函数的死循环递归调用的最终结果就是导致栈内存空间枯竭。

#include <stdio.h>
 
inline const char *num_check(int v)
{
    return (v % 2 > 0) ? "奇" : "偶";
}
 
int main(void)
{
    int i;
    for (i = 0; i < 100; i++)
        printf("%02d   %s\n", i, num_check(i));
    return 0;
}

在内部的工作就是在每个 for 循环的内部任何调用 dbtest(i) 的地方都换成了 (i%2>0)?“奇”:“偶”,这样就避免了频繁调用函数对栈内存重复开辟所带来的消耗。

  • inline使用限制
    inline 的使用是有所限制的,inline 只适合涵数体内代码简单的涵数使用,不能包含复杂的结构控制语句例如 while、switch,并且不能内联函数本身不能是直接递归函数(即,自己内部还调用自己的函数)。

  • inline仅是一个对编译器的建议
    inline 函数仅仅是一个对编译器的建议,所以最后能否真正内联,看编译器的意思,它如果认为函数不复杂,能在调用点展开,就会真正内联,并不是说声明了内联就会内联,声明内联只是一个建议而已。

  • 建议 inline 函数的定义放在头文件中
    其次,因为内联函数要在调用点展开,所以编译器必须随处可见内联函数的定义,要不然就成了非内联函数的调用了。所以,这要求每个调用了内联函数的文件都出现了该内联函数的定义。因此,将内联函数的定义放在头文件里实现是合适的,省却你为每个文件实现一次的麻烦。

  • 声明跟定义要一致:如果在每个文件里都实现一次该内联函数的话,那么,最好保证每个定义都是一样的,否则,将会引起未定义的行为。如果不是每个文件里的定义都一样,那么,编译器展开的是哪一个,那要看具体的编译器而定。所以,最好将内联函数定义放在头文件中。

  • inline 是一种"用于实现的关键字"
    关键字 inline 必须与函数定义体放在一起才能使函数成为内联,仅将 inline 放在函数声明前面不起任何作用。

inline void Foo(int x, int y); // inline 仅与函数声明放在一起
void Foo(int x, int y){}
  • 慎用 inline
    内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。
    如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
  • 以下情况不宜使用内联:
    (1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
    (2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
类中的成员函数与inline

定义在类中的成员函数默认都是内联的,如果在类定义时就在类内给出函数定义,那当然最好。如果在类中未给出成员函数定义,而又想内联该函数的话,那在类外要加上 inline,否则就认为不是内联的。

class A
{
    public:void Foo(int x, int y) {  } // 自动地成为内联函数
}
、、、、、、、、、====
// 头文件
class A
{
    public:
    void Foo(int x, int y);
}
// 定义文件
inline void A::Foo(int x, int y){}

mutable 和 volatile

mutable

mutable只能作用在类成员上,指示其数据总是可变的。不能和const 同时修饰一个成员,但能配合使用:const修饰的方法中,mutable修饰的成员数据可以发生改变,除此之外不应该对类/对象带来副作用。
考虑一个mutable的使用场景:呼叫系统中存有司机(Driver)的信息,为了保护司机的隐私,司机对外展现的联系号码每隔五分钟从空闲号码池更新一次。根据需求,Driver类的实现如下伪代码:

class Driver {
private:
 ...
 // real phone number
 string phone;
 // display phone number
 mutable string displayPhone;
public:
 string getDisplayPhone() const {
  if (needUpdate()) {
   lock.lock();
   if (needUpdate()) {
    updateDisplayPhone(); // displayPhone在这里被改变
   }
   lock.unlock();
  }
  return displayPhone;
 }
};

在上述代码中,const方法中不允许对常规成员进行变动,但mutable成员不受此限制。对Driver类来说,其固有属性(姓名、年龄、真实手机号等)未发生改变,符合const修饰。mutable让一些随时可变的展示属性能发生改变,达到了灵活编程的目的。

volatile

volatile用于修饰成员或变量,指示其修饰对象可能随时变化,编译器不要对所修饰变量进行优化(缓存),每次取值应该直接读取内存。由于volatile的变化来自运行期,其可以与const一起使用。两者一起使用可能让人费解,如果考虑场景就容易许多:CPU和GPU通过映射公用内存中的同一块,GPU可能随时往共享内存中写数据。对CPU上的程序来说,const修饰变量一直是右值,所以编译通过。但其变量内存中的值在运行期间可能随时在改变,volatile修饰是正确做法。
在多线程环境下,volatile可用作内存同步手段。例如多线程爆破密码:

volatile bool found = false;
void run(string target) {
 while (!found) {
  // 计算字典口令的哈希
  if (target == hash) {
   found = true;
   break;
  }
 }
}

volatile的修饰下,每次循环都会检查内存中的值,达到同步的效果。
需要注意的是,volatile的值可能随时会变,期间会导致非预期的结果。例如下面的例子求平方和:

double square(volatile double a, volatile double b) {
 return (a + b) * (a + b);
}

应该改成:

double square(volatile double a, volatile double b) {
 double c = a + b;
 return c * c;
}

一般说来,volatile用在如下的几个地方:

  • 中断服务程序中修改的供其它程序检测的变量需要加volatile;
  • 多任务环境下各任务间共享的标志应该加volatile;
  • 存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能有不同意义;

总结

  • mutable只能用与类变量,不能与const同时使用;在const修饰的方法中,mutable变量数值可以发生改变;
  • volatile只是运行期变量的值随时可能改变,这种改变即可能来自其他线程,也可能来自外部系统。

static

什么是static?

static 是 C/C++ 中很常用的修饰符,它被用来控制变量的存储方式和可见性。

在 C/C++ 中static的作用

总的来说
(1)在修饰变量的时候,static 修饰的静态局部变量只执行初始化一次,而且延长了局部变量的生命周期,直到程序运行结束以后才释放。
(2)static 修饰全局变量的时候,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是 extern 外部声明也不可以。
(3)static 修饰一个函数,则这个函数的只能在本文件中调用,不能被其他文件调用。static 修饰的变量存放在全局数据区的静态变量区,包括全局静态变量和局部静态变量,都在全局数据区分配内存。初始化的时候自动初始化为 0。
(4)不想被释放的时候,可以使用static修饰。比如修饰函数中存放在栈空间的数组。如果不想让这个数组在函数调用结束释放可以使用 static 修饰。
(5)考虑到数据安全性(当程序想要使用全局变量的时候应该先考虑使用 static)。

c++ static 用法

static 关键字最基本的用法是:

  • 被 static 修饰的变量属于类变量,可以通过类名.变量名直接引用,而不需要 new 出一个类来
  • 被 static 修饰的方法属于类方法,可以通过类名.方法名直接引用,而不需要 new 出一个类来

被 static 修饰的变量、被 static 修饰的方法统一属于类的静态资源,是类实例之间共享的,换言之,一处变、处处变。
在 C++ 中,静态成员是属于整个类的而不是某个对象,静态成员变量只存储一份供所有对象共用。所以在所有对象中都可以共享它。使用静态成员变量实现多个对象之间的数据共享不会破坏隐藏的原则,保证了安全性还可以节省内存。
静态成员的定义或声明要加个关键 static。静态成员可以通过双冒号来使用即 <类名>::<静态成员名>。

class Point  
{  
public:   
    void init()  
    {    
   		 printf("%d\n", m_nPointCount);//类的非静态成员函数可以调用用静态成员函数,但反之不能。
    }  
    static void output()  
    {  
   	 printf("%d\n", m_nPointCount);
    	//静态成员函数中不能引用非静态成员。
    }  
private:  
    static int m_nPointCount;
}; 
void main()  
{  
    Point pt;  
    pt.init();  //类的对象可以使用静态成员函数和非静态成员函数
    pt.output();  //
    Point::output();// 
}

总结:

(1)静态成员函数中不能调用非静态成员。
(2)非静态成员函数中可以调用静态成员。因为静态成员属于类本身,在类的对象产生之前就已经存在了,所以在非静态成员函数中是可以调用静态成员的。
(3)静态成员变量使用前必须先初始化(如 int MyClass::m_nNumber = 0;),否则会在 linker 时出错。
静态数据成员
(1)静态数据成员可以实现多个对象之间的数据共享,它是类的所有对象的共享成员,它在内存中只占一份空间,如果改变它的值,则各对象中这个数据成员的值都被改变。
(2)静态数据成员是在程序开始运行时被分配空间,到程序结束之后才释放,只要类中指定了静态数据成员,即使不定义对象,也会为静态数据成员分配空间。
(3)静态数据成员可以被初始化,但是只能在类体外进行初始化,若未对静态数据成员赋初值,则编译器会自动为其初始化为 0。
(4)静态数据成员既可以通过对象名引用,也可以通过类名引用。
静态成员函数
(1)静态成员函数和静态数据成员一样,他们都属于类的静态成员,而不是对象成员。
(2)非静态成员函数有 this 指针,而静态成员函数没有 this 指针。
(3)静态成员函数主要用来方位静态数据成员而不能访问非静态成员。

exeplicit,delete …

C++ 的类有四类特殊成员函数,它们分别是:默认构造函数析构函数、拷贝构造函数以及拷贝赋值运算符
这些类的特殊成员函数负责创建、初始化、销毁,或者拷贝类的对象。
如果程序员没有显式地为一个类定义某个特殊成员函数,而又需要用到该特殊成员函数时,则编译器会隐式的为这个类生成一个默认的特殊成员函数。
C++11 标准引入了一个新特性:"=default"函数。程序员只需在函数声明后加上“=default;”,就可将该函数声明为 "=default"函数,编译器将为显式声明的 “=default"函数自动生成函数体。
为了能够让程序员显式的禁用某个函数,C++11 标准引入了一个新特性:”=delete"函数。程序员只需在函数声明后上“=delete;”,就可将该函数禁用。

class X
{ 
public: 
    X() = default; //该函数比用户自己定义的默认构造函数获得更高的代码效率
    X(int i)
    { 
        a = i; 
    }
private: 
    int a; 
}; 

X obj;
// "=default"函数特性仅适用于类的特殊成员函数,且该特殊成员函数没有默认参数。
class X1
{
public:
    int f() = default;      // err , 函数 f() 非类 X 的特殊成员函数
    X1(int, int) = default;  // err , 构造函数 X1(int, int) 非 X 的特殊成员函数
    X1(int = 1) = default;   // err , 默认构造函数 X1(int=1) 含有默认参数
};
// "=default"函数既可以在类体里(inline)定义,也可以在类体外(out-of-line)定义。
class X2
{
public:
    X2() = default; //Inline defaulted 默认构造函数
    X2(const X&);
    X2& operator = (const X&);
    ~X2() = default;  //Inline defaulted 析构函数
};
X2::X2(const X&) = default;  //Out-of-line defaulted 拷贝构造函数
X2& X2::operator= (const X2&) = default;   //Out-of-line defaulted  拷贝赋值操作符
// 为了能够让程序员显式的禁用某个函数,C++11 标准引入了一个新特性:"=delete"函数。程序员只需在函数声明后上“=delete;”,就可将该函数禁用。
class X3
{
public:
    X3();
    X3(const X3&) = delete;  // 声明拷贝构造函数为 deleted 函数
    X3& operator = (const X3 &) = delete; // 声明拷贝赋值操作符为 deleted 函数
};
// "=delete"函数特性还可用于禁用类的某些转换构造函数,从而避免不期望的类型转换
class X4
{
public:
    X4(double)
    {
    }
    X4(int) = delete;
};
// "=delete"函数特性还可以用来禁用某些用户自定义的类的 new 操作符,从而避免在自由存储区创建类的对象
class X5
{
public:
    void *operator new(size_t) = delete;
    void *operator new[](size_t) = delete;
};
void mytest()
{
    X4 obj1;
    X4 obj2=obj1;   // 错误,拷贝构造函数被禁用
    X4 obj3;
    obj3=obj1;     // 错误,拷贝赋值操作符被禁用
    X5 *pa = new X5;      // 错误,new 操作符被禁用
    X5 *pb = new X5[10];  // 错误,new[] 操作符被禁用
    return;
}

C++中的explicit关键字只能用于修饰只有一个参数的类构造函数, 它的作用是表明该构造函数是显示的, 而非隐式的, 跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式)。

class CxString  // 没有使用explicit关键字的类声明, 即默认为隐式声明  
{  
public:  
    char *_pstr;  
    int _size;  
    CxString(int size)  
    {  
        _size = size;                // string的预设大小  
        _pstr = malloc(size + 1);    // 分配string的内存  
        memset(_pstr, 0, size + 1);  
    }  
    CxString(const char *p)  
    {  
        int size = strlen(p);  
        _pstr = malloc(size + 1);    // 分配string的内存  
        strcpy(_pstr, p);            // 复制字符串  
        _size = strlen(_pstr);  
    }  
    // 析构函数这里不讨论, 省略...  
};  
// 下面是调用:  
CxString string1(24);     // 这样是OK的, 为CxString预分配24字节的大小的内存  
CxString string2 = 10;    // 这样是OK的, 为CxString预分配10字节的大小的内存  
CxString string3;         // 这样是不行的, 因为没有默认构造函数, 错误为: “CxString”: 没有合适的默认构造函数可用  
CxString string4("aaaa"); // 这样是OK的  
CxString string5 = "bbb"; // 这样也是OK的, 调用的是CxString(const char *p)  
CxString string6 = 'c';   // 这样也是OK的, 其实调用的是CxString(int size), 且size等于'c'的ascii码  
string1 = 2;              // 这样也是OK的, 为CxString预分配2字节的大小的内存  
string2 = 3;              // 这样也是OK的, 为CxString预分配3字节的大小的内存  
string3 = string1;        // 这样也是OK的, 至少编译是没问题的, 但是如果析构函数里用free释放_pstr内

上面的代码中, “CxString string2 = 10;” 这句为什么是可以的呢? 在C++中, 如果的构造函数只有一个参数时, 那么在编译的时候就会有一个缺省的转换操作:将该构造函数对应数据类型的数据转换为该类对象. 也就是说 “CxString string2 = 10;” 这段代码, 编译器自动将整型转换为CxString类对象, 实际上等同于下面的操作:

CxString string2(10);  
或  
CxString temp(10);  
CxString string2 = temp;

但是, 上面的代码中的_size代表的是字符串内存分配的大小, 那么调用的第二句 “CxString string2 = 10;” 和第六句 “CxString string6 = ‘c’;” 就显得不伦不类, 而且容易让人疑惑. 有什么办法阻止这种用法呢? 答案就是使用explicit关键字. 即在 CxString(int size) 前面加上explicit 关键字。explicit关键字的作用就是防止类构造函数的隐式自动转换.
注意: explicit关键字只对有一个参数的类构造函数有效, 如果类构造函数参数大于或等于两个时, 是不会产生隐式转换的, 所以explicit关键字也就无效了。但是, 也有一个例外, 就是当除了第一个参数以外的其他参数都有默认值的时候, explicit关键字依然有效, 此时, 当调用构造函数时只传入一个参数, 等效于只有一个参数的类构造函数, 例子如下:

class CxString  // 使用关键字explicit声明  
{  
public:  
    int _age;  
    int _size;  
    explicit CxString(int age, int size = 0)  
    {  
        _age = age;  
        _size = size;  
        // 代码同上, 省略...  
    }  
    CxString(const char *p)  
    {  
        // 代码同上, 省略...  
    }  
};  
// 下面是调用:  
CxString string1(24);     // 这样是OK的  
CxString string2 = 10;    // 这样是不行的, 因为explicit关键字取消了隐式转换  
CxString string3;         // 这样是不行的, 因为没有默认构造函数  
string1 = 2;              // 这样也是不行的, 因为取消了隐式转换  
string2 = 3;              // 这样也是不行的, 因为取消了隐式转换  
string3 = string1;        // 这样也是不行的, 因为取消了隐式转换, 除非类实现操作符"="的重载  

virtural, final,overide

virtual在英文中表示“虚”、“虚拟”的含义。c++中的关键字“virtual”主要用在两个方面:虚函数与虚基类。下面将分别从这两个方面对virtual进行介绍。

虚函数

虚函数源于c++中的类继承,是多态的一种。在c++中,一个基类的指针或者引用可以指向或者引用派生类的对象。同时,派生类可以重写基类中的成员函数。这里“重写”的要求是函数的特征标(包括参数的数目、类型和顺序)以及返回值都必须与基类中的函数一致。如下所示:

class base
{
	int a,b;
public:
	void test(){ cout<<"基类方法!"<<endl; }
        virtual ~base(){}
};
 
class inheriter:public base
{
public:
	void test(){ cout<<"派生类方法!"<<endl; }     //重写基类方法
};

可以在基类中将被重写的成员函数设置为虚函数,其含义是:当通过基类的指针或者引用调用该成员函数时,将根据指针指向的对象类型确定调用的函数,而非指针的类型。如下,是未将test()函数设置为虚函数前的执行结果:

base *p1=new base;
base *p2=new inheriter;
p1->test();                                      //输出“基类方法”
p2->test();                                      //输出“基类方法”
在将test()函数设置为virtual后,执行结果如下:
base *p1=new base;                               //p1指向base类
base *p2=new inheriter;                          //p2指向inheriter类
p1->test();                                      //输出“基类方法”
p2->test();                                      //输出“派生类方法”

如此,便可以将基类与派生类的同名方法区分开,实现多态。
说明:

  • 只需将基类中的成员函数声明为虚函数即可,派生类中重写的virtual函数自动成为虚函数;
  • 基类中的析构函数必须为虚函数,否则会出现对象释放错误。以上例说明,如果不将基类的析构函数声明为virtual,那么在调用delete p2;语句时将调用基类的析构函数,而不是应当调用的派生类的析构函数,从而出现对象释放错误的问题。
  • 虚函数的使用将导致类对象占用更大的内存空间。对这一点的解释涉及到虚函数调用的原理:编译器给每一个包括虚函数的对象添加了一个隐藏成员:指向虚函数表的指针。虚函数表(virtual function table)包含了虚函数的地址,由所有虚函数对象共享。当派生类重新定义虚函数时,则将该函数的地址添加到虚函数表中。无论一个类对象中定义了多少个虚函数,虚函数指针只有一个。相应地,每个对象在内存中的大小要比没有虚函数时大4个字节(32位主机,不包括虚析构函数)。如下:
cout<<sizeof(base)<<endl;                        //12
cout<<sizeof(inheriter)<<endl;                   //12

base类中包括了两个整型的成员变量,各占4个字节大小,再加上一个虚函数指针,共计占12个字节;inheriter类继承了base类的两个成员变量以及虚函数表指针,因此大小与基类一致。如果inheriter多重继承自另外一个也包括了虚函数的基类,那么隐藏成员就包括了两个虚函数表指针。

  • 重写函数的特征标必须与基类函数一致,否则将覆盖基类函数;
  • 重写不同于重载。我对重载的理解是:同一个类,内部的同名函数具有不同的参数列表称为重载;重写则是派生类对基类同名函数的“本地改造”,要求函数特征标完全相同。当然,返回值类型不一定相同(可能会出现返回类型协变的特殊情况)。

虚基类

在c++中,派生类可以继承多个基类。问题在于:如果这多个基类又是继承自同一个基类时,那么派生类是不是需要多次继承这“同一个基类”中的内容?虚基类可以解决这个问题。
简而言之,虚基类可以使得从多个类(它们继承自一个类)中派生出的对象只继承一个对象。虚继承的写法如下:

class mytest:virtual public base
{
};
base称为mytest类的虚基类。假设base还是另外一个类mytest2的虚基类,对于多重继承mytest和mytest2的子类mytest3而言,base的部分只继承了一次。如下:

class base
{
	int b;
public:
	virtual void test(){ cout<<"基类方法!"<<endl; 	virtual ~base(){};
};
class mytest:virtual public base
{
};
class mytest2:virtual public base
{
};
class mytest3:public mytest,public mytest2
{
};
cout<<sizeof(mytest)<<endl;                  //输出12
cout<<sizeof(mytest2)<<endl;                 //输出12
cout<<sizeof(mytest3)<<endl;                 //输出16,若在base中添加一个int型成员,则输出20

mytest类与mytest2类的大小为什么是12?这是因为它们在虚继承自base类后,添加了一个隐藏的成员——指向虚基类的指针,占4个字节。而base类本身占8个字节,因此它们的大小均为12。而对非虚继承而言,是不需要这样的一个指针的。而mytest3类的大小为sizeof(base)+sizeof(mytest-base)+sizeof(mytest2-base),即16。

说明:
1.若一个类多重继承自具有同一个基类的派生类时,调用同名成员函数时会出现二义性。为了解决这个问题,可以通过作用域解析运算符澄清,或者在类中进行重新定义;
2.继承关系可能是非常繁复的。一个类可能多重继承自别的类,而它的父类也可能继承自别的类。当该类从不同的途径继承了两个或者更多的同名函数时,如果没有对类名限定为virtual,将导致二义性。当然,如果使用了虚基类,则不一定会导致二义性。编译器将选择继承路径上“最短”的父类成员函数加以调用。该规则与成员函数的访问控制权限并不矛盾。也就是说,不能因为具有更高调用优先级的成员函数的访问控制权限是"private",而转而去调用public型的较低优先级的同名成员函数。

纯虚函数

若一个类的成员函数被声明为纯虚函数,则意味着该类是ABC(Abstract Base Class,抽象基类),即只能被继承,而不能用来声明对象。纯虚函数通常需要在类声明的后面加上关键词“=0”。

当然,声明为纯虚函数并不意味着在实现文件中不可对其进行定义,只是意味着不可用抽象基类实现一个具体的对象。

friend

友元函数
在定义一个类的时候,可以把一些函数(包括全局函数和其他类的成员函数)声明为“友元”,这样那些函数就成为该类的友元函数,在友元函数内部就可以访问该类对象的私有成员了。

将全局函数声明为友元的写法如下:

friend  返回值类型  函数名(参数表);

将其他类的成员函数声明为友元的写法如下:

friend  返回值类型  其他类的类名::成员函数名(参数表);

但是,不能把其他类的私有成员函数声明为友元。
关于友元,看下面的程序示例。

#include<iostream>
using namespace std;
class CCar;  //提前声明CCar类,以便后面的CDriver类使用
class CDriver
{
public:
    void ModifyCar(CCar* pCar);  //改装汽车
};
class CCar
{
private:
    int price;
    friend int MostExpensiveCar(CCar cars[], int total);  //声明友元
    friend void CDriver::ModifyCar(CCar* pCar);  //声明友元
};
void CDriver::ModifyCar(CCar* pCar)
{
    pCar->price += 1000;  //汽车改装后价值增加
}
int MostExpensiveCar(CCar cars[], int total)  //求最贵气车的价格
{
    int tmpMax = -1;
    for (int i = 0; i<total; ++i)
        if (cars[i].price > tmpMax)
            tmpMax = cars[i].price;
    return tmpMax;
}
int main()
{
    return 0;
}

这个程序只是为了展示友元的用法,所以 main 函数什么也不做。
第 3 行声明了 CCar 类,CCar 类的定义在后面。之所以要提前声明,是因为 CDriver 类的定义中用到了 CCar 类型(第7行),而此时 CCar 类还没有定义,编译会报错。
不要第 3 行,而把 CCar 类的定义写在 CDriver 类的前面,是解决不了这个问题的,因为 CCar 类中也用到了 CDriver 类型(第14行),把 CCar 类的定义写在前面会导致第 14 行的 CDriver 因没有定义而报错。C++ 为此提供的解决办法是:可以简单地将一个类的名字提前声明,写法如下:

class  类名;

尽管可以提前声明,但是在一个类的定义出现之前,仍然不能有任何会导致该类对象被生成的语句。但使用该类的指针或引用是没有问题的。
第 13 行将全局函数 MostExpensiveCar 声明为 CCar 类的友元,因此在第 24 行可以访问 cars[i] 的私有成员 price。同理,第 14 行将 CDriver 类的 ModifyCar 成员函数声明为友元,因此在第 18 行可以访问 pCar 指针所指向的对象的私有成员变量 price。
友元类
一个类 A 可以将另一个类 B 声明为自己的友元,类 B 的所有成员函数就都可以访问类 A 对象的私有成员。在类定义中声明友元类的写法如下:

friend  class  类名;

来看如下例程:

class CCar
{
private:
    int price;
    friend class CDriver;  //声明 CDriver 为友元类
};
class CDriver
{
public:
    CCar myCar;
    void ModifyCar()  //改装汽车
    {
        myCar.price += 1000;  //因CDriver是CCar的友元类,故此处可以访问其私有成员
    }
};
int main()
{
    return 0;
}

第 5 行将 CDriver 声明为 CCar 的友元类。这条语句本来就是在声明 CDriver 是一个类,所以 CCar 类定义前面就不用声明 CDriver 类了。第 5 行使得 CDriver 类的所有成员函数都能访问 CCar 对象的私有成员。如果没有第 5 行,第 13 行对 myCar 私有成员 price 的访问就会导致编译错误。
一般来说,类 A 将类 B 声明为友元类,则类 B 最好从逻辑上和类 A 有比较接近的关系。例如上面的例子,CDriver 代表司机,CCar 代表车,司机拥有车,所以 CDriver 类和 CCar 类从逻辑上来讲关系比较密切,把 CDriver 类声明为 CCar 类的友元比较合理。
友元关系在类之间不能传递,即类 A 是类 B 的友元,类 B 是类 C 的友元,并不能导出类 A 是类 C 的友元。“咱俩是朋友,所以你的朋友就是我的朋友”这句话在 C++ 的友元关系上 不成立。

参考

命名空间
inline函数
mutable和volatile
static
explicit
虚函数
友元
内存分配

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++ class 的相关文章

  • Moq-ing 类或接口有什么区别?

    我一直在使用moq http code google com p moq 在我的单元测试中模拟对象 我在关于最小起订量的网站上看到它能够模拟类和接口 有一天 我与我的一位同事进行了讨论 他们表示没有理由模拟类 我应该只模拟接口 我对此并没有
  • 为什么我不能将一个类划分为多个文件

    我正在尝试创建一个类TestClass它分为几个文件 我将它分成 3 个文件 其中第一个文件TestClassPart1 php已经开始上课了class TestClass 和最后一个文件TestClassPart3 php有班级的右括号
  • 像数组一样初始化类对象

    我正在为学校项目创建一个自定义向量类 我希望能够像这样初始化它 vector x 2 3 4 5 C 有什么办法可以做到这一点吗 这是我的班级的标题 class vector private int vsize int valloc dou
  • 如何动态构造方法?

    我设计了一个类 它非常标准 具有一些方法属性 class foo def f1 self print f1 def f2 self print f2 def fn self print fn 现在我想创建一个包含一组 foo 实例的类 cl
  • 使用 javascript/jQuery 更改类的背景颜色属性

    这似乎是一个简单的问题 但没有任何解决办法 我正在尝试使用 javascript jQuery 动态更改某些文本的背景颜色 从白色或粉色到绿色 但由于某种原因它不起作用 文本使用名为 novice 的 CSS 类进行样式设置 这是CSS 这
  • 获取运行时提供的类名的 n 维数组的类

    给定一个完全限定的类名和多个维度 我想获取该类的类名 我相信我可以这样做 public Class elementType Class forName className return Array newInstance elementTy
  • 在 Delphi XE 中将类作为过程的参数传递

    我需要做的是这样的 procedure A type of form var form TForm begin form type of form Create application form showmodal freeandnil f
  • .class 与 .java

    class 文件和 java 文件有什么区别 我正在尝试让我的小程序工作 但目前我只能在 Eclipse 中运行它 还不能嵌入 HTML 谢谢 编辑 那么如何使用 JVM 进行编译呢 class 文件是编译后的 java 文件 java 都
  • 如何表示类的实例与将其作为输入的类之间的关系?

    我有一堂课叫House 这个类的实例是house class House def init self height length self height height self length length def housePlan hou
  • C# 中的类和模块有什么用

    有人可以解释一下类和模块之间的区别吗 你什么时候使用其中一种而不是另一种 我正在使用 C 更新 我的意思是相当于 VB 模块的 C 版本 这在很大程度上取决于您所指的 模块 Visual Basic 的模块 C 中没有真正等效的 VB Ne
  • 创建 OpenCV 的 mouseCallback 函数的基于类的实现时遇到问题

    正如标题所示 我在基于类的 C 结构中实现 OpenCV 的 mouseCallback 函数时遇到了一些麻烦 请允许我解释一下 我定义了一个名为 BriskMatching 的类 在其中创建了一个名为 mouseCallback 的成员函
  • C++类名冲突

    我现在正在做一个项目 需要整合两个子项目 项目A是用C 编写的 项目B是用C编写的 一个问题是 在项目B中 有一个名为vector它是由其作者创建的 在项目 A 中 std vector in STL用来 因为项目B以后可能会更新 所以我不
  • 使类只能从特定类实例化

    假设我有 3 节课class1 class2 and class3 我怎样才能拥有它class1只能通过实例化class2 class1 object new class1 但不是 class3 或任何其他类 我认为它应该与修饰符一起使用
  • 为什么基类必须有一个带有 0 个参数的构造函数?

    这不会编译 namespace Constructor0Args class Base public Base int x class Derived Base class Program static void Main string a
  • Delphi - 如果没有创建类,为什么这个函数可以工作?

    考虑这个类 unit Unit2 interface type TTeste class private texto string public function soma a b integer string end implementa
  • 类方法作为 JavaScript 中的事件处理程序?

    JavaScript 中是否有最佳实践或通用方法将类成员作为事件处理程序 考虑以下简单示例
  • 错误:无效使用不完整类型“类 Move”/未定义对 Move::NONE 的引用

    拜托 我不知道为什么这个简单的代码被拒绝 它给了我 2 个编译错误 请帮帮我 I use 代码 块 20 03 我的编译器是GNU GCC 移动 hpp class Move public Move Move int int public
  • Swift 中的字典是否应该转换为类或结构?

    我正在开发一个本机 iOS 应用程序 该应用程序从我们也可以控制的 Web 服务接收 JSON 格式的数据 该计划是在大约 18 个月内更换后端数据库 以支持不同的平台 考虑到这一点 我们希望确保 iOS 应用程序能够相对容易地适应新的数据
  • Ruby/Rails - 如何创建类并从控制器访问它

    我一直在尝试使用一些不同的 gem 在 Rails 3 中显示谷歌地图 但遇到了一些问题 幸运的是我发现了这个https github com YouthTree bhm google maps https github com Youth
  • 如何在 PHP >= 5.3 严格模式下向对象添加属性而不产生错误

    这必须很简单 但我似乎找不到答案 我有一个通用的 stdClass 对象 foo没有属性 我想添加一个新属性 bar尚未定义 如果我这样做 foo new StdClass foo gt bar 1234 严格模式下的 PHP 会抱怨 将属

随机推荐

  • ArduPilot——如何对飞控LOG进行简易振动分析

    版权声明 xff1a 本文为博主原创博文 xff0c 未经允许不得转载 xff0c 若要转载 xff0c 请说明出处并给出博文链接 首先 xff0c 你得先有一架可以飞的且刷的是ArduPilot飞控代码的无人机和地面站Misson Pla
  • SITL Simulator —— ArduPilot —— Windows

    版权声明 xff1a 本文为博主原创博文 xff0c 未经允许不得转载 xff0c 若要转载 xff0c 请说明出处并给出博文链接 参考网页 xff1a http ardupilot org dev docs sitl native on
  • ArduCopter——ArduPilot——航点导航WPNav(一)

    版权声明 xff1a 本文为博主原创博文 xff0c 未经允许不得转载 xff0c 若要转载 xff0c 请说明出处并给出博文链接 现如今 xff0c 四旋翼飞行器已经从几年前的遥控航模变成真正可以超视距操控的无人机 xff0c 离不开伟大
  • 多频超声波清洗换能器用于高精密清洗系统

    多频超声波清洗换能器因加工方式和工作要求不同 xff0c 超声波换能器的工作方式可分为连续工作和脉冲式工作 xff0c 不同的工作方式对换能器的要求是不同的 连续式工作是不停机工作 xff0c 工作电流不是很大 xff0c 一般使用在清洗方
  • 超声波发生器电源控制电路线路板设计

    超声波发生器电源控制电路线路板是由匹配电容 xff0c 驱动变压器 xff0c 高功率的IGBT功率管 xff0c 匹配电感盒MOS管组成 xff0c 所有的组件一目了然 xff0c 零部件少 xff0c 故障率低 xff0c 造价成本低
  • 洗碗机超声波换能器振子设计

    超声波的声波是一种可以穿透液体和固体的声学化学能量 xff0c 超声波的污染非常小穿透力强 xff0c 所以人们就想到了用它来清洗餐具 xff0c 于是乎超声波洗碗机就应运而生了 洗碗机超声波换能器振子作为超声波洗碗机的三大组件之一 xff
  • 超声波清洗机电路板线路板设计

    在我们日常生活中遇见的超声波清洗设备 xff0c 大多是使用低频率或低功率的超声波 xff0c 而且由于被清洗件尺寸往往较小 xff0c 所以清洗槽的尺寸往往也较小 xff0c 清洗槽内壁高度一般不高于400mm 这类清洗机清洗物品涉及范围
  • 在Mac中开发STM32单片机

    想要在Mac下开发32单片机 xff0c 又不想装虚拟机的同学可以看看 使用工具 xff1a CLion xff08 JetBrains家族一员 xff09 STM32CubeMX xff08 配置芯片 xff0c 生成初始化代码 xff0
  • 超声波电路板驱动线路设计

    超声波电路板驱动线路通电交流电压220V 10 xff0c 电源电压需稳定 体积小 占用空间小 功率大 清洗效果好 自动化程度高 超声波电路板驱动线路驱动超声清洗换能器依次启动低频初洗 中频精洗 换水 高频漂洗 排水等工序 超声波发生器的输
  • 小型超声波PCB电路板设计

    小型超声波PCB电路板通过自动频率跟踪技术 PWM调功控制技术 电容阻抗匹配技术来匹配超声波清洗换能器 小型超声波PCB电路板在驱动负载工作时 xff0c 负载参数会发生变化 xff0c 这就是需要小型超声波PCB电路板能及时捕捉到新的谐振
  • 压电陶瓷超声波换能器设计

    压电陶瓷超声波换能器是值由电能通过压电陶瓷片转化为机械能 xff0c 通过结构件放大传播出去的一种机械运动 压电陶瓷超声波换能器的压电陶瓷片尺寸越大 xff0c 输出的功率越大 xff0c 相应的频率越低 xff0c 而尺寸越小频率相对应的
  • 数字式小型超声波清洗机设计

    数字式小型超声波清洗机可以超声 加热 定时LED数码显示 xff0c 整机为不锈钢结构 使用新型加热方式 xff0c 有效提升加热效果 xff1b 结构工艺上提升防漏水等级 xff0c 同时兼容无效热能导出结构 xff1b 压花处理不锈钢工
  • 超声波清洗振板盒带发生器设计

    超声波清洗振板盒带发生器投入式清洗 xff0c 方便携带移动使用 xff1b 时间可调 xff1a 1 99分钟可调 xff0c 可常开 xff1b 功率可调 xff1a 10 100 可调 xff1b 304外壳 xff1a SUS304
  • PE塑料超声波焊接机设备设计

    聚乙烯 xff08 polyethylene xff0c 简称PE xff09 是乙烯经聚合制得的一种热塑性树脂 在工业上 xff0c 也包括乙烯与少量 烯烃的共聚物 聚乙烯无臭 xff0c 无毒 xff0c 手感似蜡 xff0c 具有优良
  • jetson nx fan auto pwm

    参考 xff1a Jetson Nano PWM自动调速 Ay yzx的博客 CSDN博客 代码 xff1a usr bin env python import os import commands from the path of thi
  • 关于github px4 gps 驱动的开发的总结

    源码编译上边已经写过文章了 遇到的几个问题 1 解决虚拟机不能共享文件夹的问题 一开始虚拟机的更新 vmware tools 是灰色的 xff0c 不能点 xff0c 然后通过关掉虚拟机 xff0c 然后再开启的时候 xff0c 在没有启动
  • 解决Android Studio 安装gradle失败、很慢

    拉取新的项目工程是 xff0c 总是会遇到gradle 下载特别特别慢 xff0c 而且经常都是等了很久最终还以失败告终 这时 xff0c 我们可以使用浏览器自己去下载gradle xff0c 或者找同事要一份相同版本的gradle xff
  • 基于数传电台的多主通讯系统的实现

  • JNI基础简介

    导读 在前面的几篇文章中 xff0c 笔者介绍了C 43 43 中的指针 引用 智能指针 多线程 类型转换 异常处理等相关知识点 xff0c 如果想要熟练掌握 xff0c 并能在实际项目中运用 xff0c 光是看肯定是毫无用处 xff0c
  • C++ class

    namespace 在变量或函数前面加上命名空间 xff0c 用来区分其它位置中的同名函数或变量 span class token macro property span class token directive keyword incl