• 类(class)


            类是 C++中一个非常重要的元素,可以说是 C++的灵魂所在了,我们都知道 C++说一种面向对象的编程语言,那么面向对象是一种什么概念呢?在 C++程序设计中,所有一切东西都可以称之为对象,任何对象都应该具有属性和行为。例如一台电脑,属性有电脑的相关配置,功能是可以玩游戏,可以打字等等。而类就是描述一个对象的一种概念,例如我们创建一个人类,那么人这个类其实就是一种概念,并不是实际的某个人,而是人的一些特征集合,具体到某个人,例如张三,那么张三就是一个对象,他有名字有身高体重,行为可以跑步,可以吃饭等等。所以我们一定要理解类其实就是保存一类对象的特征的集合。
            在 c 语言中,我们来表示一个东西的特征的时候,我们使用的是结构体来表示,例如:表示一个人
    1. struct person
    2. {
    3. char name[16]; //名字
    4. float height; //身高
    5. float height; //体重
    6. char cell[32]; //手机号
    7. };
            定义号一个人的属性之后,如果想对这个人有所操作的话,就要去定义函数,然后去在主函数中去实现一些函数的过程逻辑,所以说 c 语言是一种面对过程编程的语言,而在 C++中,对结
    构体进行了一个升级,可以在里面去定义函数来表示这类对象的一些行为,同时可在里面设置一些权限实现封装,使其内部元素对外隐藏,只提供一些对外接口和外部对接,这就是 C++的一个封装性。
    1. class person
    2. {
    3. private:
    4. char name[16]; //名字
    5. float height; //身高
    6. float weight; //体重
    7. char cell[32]; //手机号
    8. public:
    9. int running(char *WayName,int time); //跑步行为
    10. int eat(char *ObjectName,int l); //存放行为
    11. };

    class 和 struct 在 C++中的区别

    在 C++中同样也是可以使用 struct 来表示类的,它们所具有的功能完全一致,只有以下几点不同:
    1.默认继承权限
            使用 struct 默认继承权限是公有的(public),而 class 默认继承权限是私有的(private)
    2.默认访问权限
            struct 的默认访问权限是公有的,而 class 的默认访问权限是私有的。

    类的访问修饰符

    访问修饰符描述了类中成员的访问权限(哪些成员是可以直接访问的,哪些不能访问)
    [public]:
            public 修饰的成员,称为公有成员,在任何地方都可以直接访问
    [private]:
            private 修饰的成员,称为私有成员,只有本类的成员函数和友元函数才可以访问
    [protected]:
            protected 修饰的成员,称为受保护的成员,只有本类的成员函数和友元函数和它的派生类才可以访问
            一般来说,我们一般将类的属性信息设置为私有的,外界不可访问,这样就保护了我们的对象不被轻易改变,而把一些行为接口设置为公有的供外部去对我们的这个对象进行操作。

    构造函数和析构函数

            在类中,默认会有一个构造函数和一个析构函数,在定义对象的时候,程序会自动去执行构造函数对对象进行初始化,释放对象时会自动调用析构函数去对对象进行释放,构造函数的名字和类名一样,没有返回值,析构函数名字则是在类名的前面加上~号,无参数也没有返回值,程序员也可以重写构造函数和析构函数,为对象中的属性值进行初始化和释放,例如:
    1. #include
    2. #include
    3. using namespace std;
    4. class person
    5. {
    6. private:
    7. char name[16]; //名字
    8. float height; //身高
    9. float weight; //体重
    10. char cell[32]; //手机号
    11. public:
    12. person();
    13. person(const char *ne,float h,float w,const char *ce);
    14. ~person()
    15. {
    16. cout << name <<":执行了析构" << endl;
    17. }
    18. void run()
    19. {
    20. cout << "名字:"<< name << endl;
    21. cout << "身高:"<< height << endl;
    22. cout << "体重:"<< weight << endl;
    23. cout << "手机号:"<< cell << endl;
    24. }
    25. int running(char *WayName,int time); //跑步行为
    26. int eat(char *ObjectName,int l); //存放行为
    27. };
    28. person::person()
    29. {
    30. strcpy(name,"zhangsan");
    31. height = 168.4;
    32. weight = 64.2;
    33. strcpy(cell,"17873432557");
    34. }
    35. person::person(const char *ne,float h,float w,const char *ce)
    36. {
    37. strcpy(name,ne);
    38. height = h;
    39. weight = w;
    40. strcpy(cell,ce);
    41. }
    42. int main()
    43. {
    44. person a; //创建一个对象,执行的是没有参数的构造函数
    45. person b{"jiuyue",178.7,76.6,"110"}; //创建一个对象执行有参数的构造函数
    46. a.run();
    47. b.run();
    48. return 0;
    49. }

            构造函数可以传递参数对类中成员进行赋值, 也可以不传递,实例化对象时,程序会利用函数重载自动匹配调用哪个构造函数,但是析构函数不同了,析构函数是不可以带参数的。
    注意: 在实例化对象的时候,如果想要程序去匹配没有参数的构造函数时,一定不要这样写
    person a ();
    系统会认为你这是一个函数声明,不会认为你这是一个对象的实例化,所以你只能是以下写法:
    person a ;
    //
    person a {};

    构造函数初始化列表

    构造函数的初始化列表是对构造函数的一种升级,以下情况必须用初始化列表:
            1.成员对象没有默认构造函数
            2.成员变量是常量
            3.成员变量是引用
            4.初始化基类的成员
            构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。例如:
    1. class CExample
    2. {
    3. public:
    4. int a;
    5. float b;
    6. //构造函数初始化列表
    7. CExample(): a(0),b(8.8)
    8. {}
    9. //构造函数内部赋值
    10. CExample()
    11. {
    12. a=0;
    13. b=8.8;
    14. }
    15. };
            上面的例子中两个构造函数的结果是一样的。上面的构造函数(使用初始化列表的构造函数)显式的初始化类的成员;而没使用初始化列表的构造函数是对类的成员赋值,并没有进行显式的初始化。

    我们来看第一种情况为什么要使用构造函数初始化列表:(成员对象没有默认构造函数)

    1. #include
    2. #include
    3. using namespace std;
    4. class A
    5. {
    6. private:
    7. int a;
    8. char b;
    9. public:
    10. A(int x,char y)
    11. {
    12. a = x;
    13. b = y;
    14. cout << "a 的构造" << endl;
    15. }
    16. };
    17. class B
    18. {
    19. private:
    20. A a;
    21. int c;
    22. public:
    23. B(int z,char n,int x):a(z,n){ c = x;}
    24. };
    25. int main(int argc, char const *argv[])
    26. {
    27. B b(1,'2',3);
    28. return 0;
    29. }
            例如上面程序,如果 B 类中有成员变量 A 类,B 类想要在构造函数中去对 a 进行初始化赋 值,如果不使用构造函数初始化列表的方式,就无法做到在构造函数内部去调用 a 的构造函数去对 a 进行初始化。
    再来看第二种情况和第三种情况(成员变量是常量或引用)
    1. #include
    2. #include
    3. using namespace std;
    4. class A
    5. {
    6. private:
    7. int a;
    8. const int b;
    9. char& c;
    10. public:
    11. A(int x,char y):b(x),c(y)
    12. {
    13. a = 3;
    14. //b = 4; //const 常量只能在声明时初始化,之后不能改变
    15. //c = ? //引用只能在声明的时候进行初始化
    16. }
    17. };
    18. int main(int argc, char const *argv[])
    19. {
    20. char i = '1';
    21. A(4,i);
    22. return 0;
    23. }
            如上程序,在类 A 中有常量成员和引用成员,因为这两个成员只能在声明时进行赋值初始 化,其他时候都不能对其值进行改变,所以必须使用构造函数初始化列表那么最后一种就容易理解了(初始化基类的成员),因为子类无法访问基类的私有成员,所以必然不可能在构造函数体中对基类成员进行初始化,只能使用初始化成员列表对基类初始化,例如下面程序。
    1. #include
    2. #include
    3. using namespace std;
    4. class A
    5. {
    6. private:
    7. int a;
    8. const int b;
    9. char& c;
    10. public:
    11. A(int x,char y):b(x),c(y)
    12. {
    13. a = 3;
    14. //b = 4; //const 常量只能在声明时初始化,之后不能改变
    15. //c = ? //引用只能在声明的时候进行初始化
    16. }
    17. };
    18. class B:public A
    19. {
    20. private:
    21. int x;
    22. public:
    23. B(int a1,char b1):A(a1,b1)
    24. {
    25. x = 2;
    26. }
    27. };
    28. int main(int argc, char const *argv[])
    29. {
    30. char i = '1';
    31. B(4,i);
    32. return 0;
    33. }

    结论

    其实通过上面的学习我们大概了解了构造函数初始化列表的作用了,简单来说就是:
    1. int a = 3;
    2. const char c = '1';
    3. A b(2,5);
    上面这种初始化方式为显性赋值,因为它们是声明时就初始化了。
    1. int a;
    2. const char c;
    3. A b;
    4. a = 3;
    5. c = '1' //error
    6. b(3,5); //error
            而上面这种赋值就属于隐性赋值,可以看到,很多东西是不允许隐性赋值的,例如常量, 引用,类。
    而对构造函数进行初始化成员列表进行初始化其实就是对类的成员进行显性赋值

    临时对象

            临时对象是指在函数传参或者函数返回的时候,临时创建的,没有名字的对象,用完以后会立即销毁这种对象。

    例如:

    1. #include
    2. #include
    3. using namespace std;
    4. class A
    5. {
    6. private:
    7. int a;
    8. int b;
    9. public:
    10. A(int x,int y):a(x),b(y)
    11. {
    12. cout << "构造" <
    13. }
    14. ~A()
    15. {
    16. cout << "析构" <
    17. }
    18. A fun()
    19. {
    20. return A(a,b);
    21. }
    22. };
    23. int main(int argc, char const *argv[])
    24. {
    25. A i = A(1,2);
    26. A j = i.fun();
    27. return 0;
    28. }
    直接调用构造函数将产生一个临时对象。
    临时对象的声明周期只有一条语句的时间。
    临时对象的作用域只在一条语句中。
    临时对象是 C++中值得警惕的灰色地带。

    类的只读成员函数

            在 c++中我们知道 const 可以用来指定变量为只读变量,那么在 C++中,我们还可以使用 const 来修饰类中的成员函数,例如:
    1. class A
    2. {
    3. private:
    4. int a;
    5. int b;
    6. public:
    7. A(int x,int y):a(x),b(y){}
    8. int fun() const
    9. {
    10. //...
    11. }
    12. };
            如上程序,类中的 fun()函数被 const 修饰后,那么在 fun()函数中将不能对类的成员变量进行改变。这就是 const 修饰类中成员函数的作用。

    拷贝构造函数

            拷贝构造函数是一种特殊的构造函数,它在创建对象时, 是使用同一类中之前创建的对象 来初始化新创建的对象 。拷贝构造函数通常用于:
            •通过使用另一个同类型的对象来初始化新创建的对象。
            •复制对象把它作为参数传递给函数。
            •复制对象,并从函数返回这个对象。
    函数形式:
    类名 (const 类名 &)
    注意:
            在类中有一个默认的赋值操作,容易和拷贝构造搞混淆,赋值操作是当= 两边都是已经初始化好的对象进行赋值时才会调用赋值函数,而当用一个已经初始化好的对象去初始化另一个对象时,那么另一个对象就调用的是拷贝构造函数。
            如果在类中没有定义拷贝构造函数,编译器会自行定义一个。如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。拷贝构造函数的最常见形式如下:
    1. class A
    2. {
    3. private:
    4. int a;
    5. int b;
    6. public:
    7. A(int x,int y):a(x),b(y){} //构造函数初始化列表
    8. A(const A &obj) //拷贝构造函数
    9. {
    10. // 构造函数的主体
    11. }
    12. };
    当对对象进行拷贝赋值时就会调用拷贝构造函数进行初始化赋值,如
    1. int main(int argc, char const *argv[])
    2. {
    3. A i(1,2);
    4. A j = i; //调用了拷贝构造
    5. A k(i); //调用了拷贝构造
    6. return 0;
    7. }

    省略复制

            复制省略是自 C++11 标准起提出的一项编译优化技术,通过省略复制及移动构造函数的调用,实现零复制的值传递语义。省略复制一般默认存在,可以通过在编译时添加-fno-elide
    constructors 选项来关闭省略复制 。在以下两种情况中会出现省略复制。
    1. 在变量初始化中,当初始化表达式 ( 右值 ) 与变量类型为同一类型的纯右值 ( 临时量 ) 时,
    例如:
    1. #include
    2. #include
    3. using namespace std;
    4. class A
    5. {
    6. private:
    7. char buf[16];
    8. public:
    9. A(const char *str)
    10. {
    11. strcpy(buf,str);
    12. cout << "构造函数" << endl;
    13. }
    14. A(const A& a)
    15. {
    16. strcpy(this->buf,a.buf);
    17. cout << "拷贝构造" << endl;
    18. }
    19. };
    20. int main()
    21. {
    22. A ob = "nihao";
    23. return 0;
    24. }

    上面是进行了省略构造后的结果,那关闭省略构造呢?

            可以看到,不进行省略复制就会执行拷贝构造函数,省略复制相当于把 A ob = “nihao” 变成了 A ob( nihao ); 直接将对象构造到它们本来要拷贝的存储空间 , 而不进行省略复制, A ob = “nihao”相当于 A ob = A(“nihao”)
    2. 第二种情况是在 return 语句中,操作的对象是与函数返回类型为同一类型的纯右值 ( 临时量) 时,例如:
    1. #include
    2. #include
    3. using namespace std;
    4. class A
    5. {
    6. private:
    7. char buf[16];
    8. public:
    9. A(const char *str)
    10. {
    11. strcpy(buf,str);
    12. cout << "构造函数" << endl;
    13. }
    14. A(const A& a)
    15. {
    16. strcpy(this->buf,a.buf);
    17. cout << "拷贝构造" << endl;
    18. }
    19. };
    20. A fnn()
    21. {
    22. return A("nihao");
    23. }
    24. //或
    25. A fuu()
    26. {
    27. A a("nihao");
    28. return a;
    29. }
    30. int main()
    31. {
    32. A ob = fuu();
    33. return 0;
    34. }

    上图是进行了省略复制的结果

            上图是不进行省略复制的结果,可以看到,原本的 A ob = fuu(); 在不进行省略复制的情况 下,编译器会被解释为 A ob = A(fuu());

    this 指针

            在 C++ 中,每一个对象都能通过 this 指针来访问自己的地址。 this 指针是所有成员函数的隐含参数( 也就是所有成员函数都默认有一个隐藏的指针参数保存着对象的地址 ) 。因此,在成员函数内部,它可以用来指向调用对象。
            友元函数没有 this 指针,因为友元不是类的成员。只有成员函数才有 this 指针。静态成员函数也没有 this 指针,因为静态成员函数不属于某个对象,只属于类本身。
    1. class A
    2. {
    3. private:
    4. int a;
    5. int b;
    6. public:
    7. A(int x,int y):a(x),b(y){} //构造函数初始化列表
    8. void fun()
    9. {
    10. this->a = 4; //this 就是该对象的地址
    11. (*this).b = 5; //*this 就相当于对象本身
    12. }
    13. };
            如上,我们可以在成员函数中利用 this 来操作成员变量,至于这个 this 是谁的地址,那就要看是哪个对象了,例如:a.fun();那么 this 就指向 a 这个对象,b.fun();那么 this 就指向 b 这个对象,this 其实用处很多,如下面程序:
    1. class A
    2. {
    3. private:
    4. int a;
    5. int b;
    6. public:
    7. A(int x,int y):a(x),b(y){} //构造函数初始化列表
    8. void fun(int a,int b)
    9. {
    10. //a = a; //erreor ,系统会以为是形参 a 赋值给形参 a,并不会改变成员变量的值
    11. //b = b; //同上理
    12. this->a = a;
    13. this->b = b;
    14. }
    15. };
            当参数的名字和成员的名字相同时,那么就必须使用 this 去做区分了。
            总之,我们理解 this 是成员函数中隐藏的一个指向对象地址的一个指针就可以了,就相当于原本类中的函数应该都是下面这样的:
    void sun ( A * this );
    void fun ( int a , int b , A * this );
            每个函数中都必须有一个类指针 this,当调用它时,就会默认传入调用者对象的地址,如 下:
    A a ( 1 , 2 );
    a . sun (& a );
    a . fun ( 1 , 2 ,& a );
    但是,你可以理解为系统将这些东西都隐藏了。

    类的静态成员函数

            静态成员函数是使用 static 修饰的成员函数,只能被定义一次,而且要被同类的所有对象所共享,它是类的一种行为,与对象无关。
    1. class person
    2. {
    3. public:
    4. static void func(); //只需要在类内进行声明
    5. };
    它有如下特点: 1. 静态成员函数不可以直接访问类中非静态数据成员以及非静态成员函数,只能通过对象名(由参数传入)来访问,即在类的静态成员函数(类的静态方法)内部,不能直接访问 this 指针和对象的数据成员,在类的静态成员函数(类的静态方法)内部,只能访问类的静态数据
    成员!
    2. 静态成员函数在类外定义时,无需加 static 修饰,否则出错;

    3. 在类外,可以通过对象名以及类名来调用类的静态成员函数和变量
    4. 对象和类可以直接访问静态成员函数,静态成员之间在类中可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数;
    5. 在类外,可以通过对象名以及类名来调用类的静态成员函数,静态成员函数不能访问非静态成员函数和非静态数据成员;
    6. 声明静态成员函数时,不可同时声明为 virtual const volatile 函数
    7. 静态函数可以使用作用域标识符直接访问,无需创建任何对象就可以访问
    1. int main(int argc, char const *argv[])
    2. {
    3. A::fun();
    4. return 0;
    5. }
    8. 类的静态成员变量必须在类外进行初始化才能被使用,否则报错,例如:
    1. class A
    2. {
    3. private:
    4. int a;
    5. int b;
    6. static int c;
    7. public:
    8. static void fun();
    9. };
    10. int A::c = 0; //静态成员变量初始化
    可以使用对象操作静态成员变量,也可以使用类本身,前提是静态成员变量是公有的,如下程序:
    1. #include
    2. #include
    3. using namespace std;
    4. class A
    5. {
    6. private:
    7. int a;
    8. int b;
    9. public:
    10. static int c;
    11. static void fun()
    12. {
    13. cout << c << endl;
    14. }
    15. };
    16. int A::c = 0;
    17. int main(int argc, char const *argv[])
    18. {
    19. A a;
    20. a.c = 0;
    21. A::c = 5;
    22. a.fun();
    23. return 0;
    24. }

    友元函数和友元类(friend)

            按 c++的封装性来说,最主要的目的就是确保数据的安全,实现信息的隐藏,从原则上来 说,类的私有成员和保护成员,在类的外部是不能直接访问的,但是,有一个例外,这个就是友元(friend),友元就是在类的声明中,用关键字 friend 修饰的函数或者类,友元授予一个函数或者一个类特权,允许他们能直接访问本类的隐藏信息.

    友元函数

            友元函数不是类的成员函数,但是仍可以访问该类的私有成员。友元函数可以是一个外部函数,也可以是另一个类的成员函数。
            友元函数可以访问类中的私有成员和其他数据,但是访问不可直接使用数据成员,需要通 过对对象进行引用,所以友元函数必须有一个参数用来传递对象的地址或引用
    当友元函数是全局类外函数时在类中声明的格式:
    friend 返回值类型 函数名(参数列表);
    如下是当类外函数做友元的示例:
    1. #include
    2. #include
    3. using namespace std;
    4. class A; //对 A 进行超前声明,不然 sun 参数找不到
    5. int sun(int a,int b,A& c);
    6. class A
    7. {
    8. private:
    9. int x;
    10. int y;
    11. public:
    12. A(int a,int b):x(a),y(b){}
    13. ~A(){}
    14. friend int sun(int a,int b,A& c);
    15. };
    16. int sun(int a,int b, A& c)
    17. {
    18. c.x = a;
    19. c.y = b;
    20. return c.x+c.y;
    21. }
    22. int main(int argc, char const *argv[])
    23. {
    24. A a(2,4);
    25. sun(1,2,a);
    26. return 0;
    27. }
            特别要注意的是,在对函数声明时,一定要对类进行超前声明,因为在对函数声明中有个参数是这个类,不进行超前声明函数会找不到这个类的定义。还有在友元函数在调用上同一般函数一样,不必通过对对象进行引用。
            当友元函数是另外一个类中的成员函数时在类中声明的格式:
    friend 返回值类型 类名::函数名(参数列表);
    如下是当其他类的成员函数做友元的示例:
    1. #include
    2. #include
    3. using namespace std;
    4. class A; //对 A 进行超前声明,不然 sun 参数找不到
    5. class B
    6. {
    7. private:
    8. int i;
    9. char j;
    10. public:
    11. B(int a,char c)
    12. {
    13. i = a;
    14. j = c;
    15. }
    16. ~B(){}
    17. int fun(A *rm);
    18. };
    19. class A
    20. {
    21. private:
    22. int x;
    23. int y;
    24. public:
    25. A(int a,int b):x(a),y(b){}
    26. ~A(){}
    27. friend int B::fun(A* rm); //友元
    28. };
    29. int B::fun(A *rm)
    30. {
    31. i = rm->x + rm->y;
    32. j = 'a';
    33. return i;
    34. }
    35. int main(int argc, char const *argv[])
    36. {
    37. A a(2,4);
    38. B b(2,'A');
    39. b.fun(&a);
    40. return 0;
    41. }

    友元类

            可以将一个类定义为另一个类的友元,这样,被定义为友元的类中的函数就可以访问另一个类中的所有内容了, 这样是不安全的。最好的方法就是哪个需要访问,就设置为友元函数即可。
    当其他类是友元类时在类中声明的格式:
    friend 类名;
    如下是当其他类做友元的示例:
    1. #include
    2. #include
    3. using namespace std;
    4. class A; //对 A 进行超前声明,不然 sun 参数找不到
    5. class B
    6. {
    7. private:
    8. int i;
    9. char j;
    10. public:
    11. B(int a,char c)
    12. {
    13. i = a;
    14. j = c;
    15. }
    16. ~B(){}
    17. int fun(A &a);
    18. };
    19. class A
    20. {
    21. private:
    22. int x;
    23. int y;
    24. friend B;
    25. public:
    26. A(int a,int b):x(a),y(b){}
    27. ~A(){}
    28. };
    29. int B::fun(A &a)
    30. {
    31. i = a.x + a.y;
    32. return i;
    33. }
    34. int main(int argc, char const *argv[])
    35. {
    36. A a(2,4);
    37. B b(2,'A');
    38. b.fun(a);
    39. return 0;
    40. }

    友元注意事项

            1.友元关系是单向的,如 类 A 声明它有一个朋友 B,即 B 是 A 的朋友,反过来,A 不一定是 B 的朋友
            2. 友元关系是不可传递的,如 A 是 B 的朋友,B 是 C 的朋友,A 不一定是 C 的朋友
            3. 一个函数可以是多个类友元函数。
  • 相关阅读:
    Kafka必问面试题
    配置管理新纪元:Eureka引领分布式服务配置潮流
    关于WebMvcConfigurer
    8000字让你知道NoSQL是什么
    Blueprints - 虚幻中的行为树(Behavior Tree)
    牛视系统源码,抖音矩阵系统。抖音SEO源码。mmm
    Spark学习(2)-Spark环境搭建-Local
    js 设计模式 (面向对象编程)(第一部分)
    可编程渲染管线(Scriptable Render Pipeline, SRP)
    Mybatis快速入门
  • 原文地址:https://blog.csdn.net/jiu_yue_ya/article/details/134253527