• 小甲鱼C++笔记(下)25-48


    二十五  二十六  二十七  重载

    运算符重载

    1. 作为成员函数

     1 #include <iostream>
     2 
     3 using namespace std;
     4 
     5 class Add
     6 {
     7 private:
     8     double a;
     9     double b;
    10 public:
    11     Add();
    12     Add(double,double);
    13     Add operator+(Add num2);
    14     void print();
    15 };
    16 
    17 Add::Add()
    18 {
    19 }
    20 
    21 Add::Add(double a,double b)
    22 {
    23     this->a = a;
    24     this->b = b;
    25 }
    26 
    27 Add Add::operator+(Add num2)
    28 {
    29     Add num3;
    30     num3.a = this->a + num2.a;
    31     num3.b = this->b + num2.b;
    32     return num3;
    33 }
    34 
    35 void Add::print()
    36 {
    37     cout<<"<"<<a<<","<<b<<"i>"<<endl;
    38 }
    39 
    40 int main()
    41 {
    42     Add num1(2,4);
    43     Add num2(3,5);
    44     Add num3;
    45 
    46     num3 = num1 + num2;
    47     num1.print();
    48     num2.print();
    49     num3.print();
    50     return 0;
    51 }

    注意除了以下几个运算符不能重载外,其他都可以

    .  .*  ::  sizeof  ?:

    重载不能改变运算符的运算对象(操作数)个数

    重载不能改变运算符的优先级别

    重载不能改变运算符的结合性

    重载运算符的函数不能有默认的参数

    重载的运算符必须和用户的自定义类型的对象一起使用,其参数至少有一个是类对象或类对象的引用(也就是说参数不能全是C++的标准类型,这样约定是为了防止用户修改用于标准类型结构的运算符的性质)

    解释operate+(Add)为什么只有一个参数

    对于num1 +num2系统解释为num1.operate+(Add num2)

    c++ primer:p431 作为类成员的重载函数,其形参看起来不操作数数目少1。作为成员函数的操作符有一个隐含的this形参,限定为第一个操作数

    2. 作为非成员函数,通常必须将他们设置为所操作类的友元

    作为友元函数就要显示声明两个参数了

    二十八 二十九   多继承 虚继承

     一般来说不建议使用多继承,虚继承一般是伴随多继承使用的

    多继承格式:

    虚继承格式:

    虚继承的作用是:通过虚继承某个基类,就是告诉编译器,从当前这个类再派生出来的子类只能拥有那个基类的一个实例

    虚继承的使用是为了避免多继承的二义性,一个虚继承的完整例子:

     1 #include <iostream>
     2 
     3 using namespace std;
     4 
     5 #include <string>
     6 
     7 class Person
     8 {
     9 public:
    10     Person(string);
    11     virtual void introduce() = 0;
    12 protected:
    13     string name;
    14 };
    15 
    16 Person::Person(string name)
    17 {
    18     this->name = name;
    19 }
    20 
    21 class Teacher:virtual public Person
    22 {
    23 public:
    24     Teacher(string,string);
    25     virtual void introduce();
    26 protected:
    27     string teach_class;
    28 };
    29 
    30 Teacher::Teacher(string name,string teach_class):Person(name)
    31 {
    32     this->teach_class = teach_class;
    33 }
    34 
    35 void Teacher::introduce()
    36 {
    37     cout<<"Teacher "<<name<<" is teaching "<<teach_class<<endl;
    38 }
    39 
    40 class Student:virtual public Person
    41 {
    42 public:
    43     Student(string,string);
    44     virtual void introduce();
    45 protected:
    46     string learn_class;
    47 };
    48 
    49 Student::Student(string name,string learn_class):Person(name)
    50 {
    51     this->learn_class = learn_class;
    52 }
    53 
    54 void Student::introduce()
    55 {
    56     cout<<"Student "<<name<<" is learning "<<learn_class<<endl;
    57 }
    58 
    59 class Assistant:public Teacher,public Student
    60 {
    61 public:
    62     Assistant(string,string,string);
    63     void introduce();
    64 };
    65 
    66 Assistant::Assistant(string name,string teach_class,string learn_class):Teacher(name,teach_class),Student(name,learn_class),Person(name)
    67 {
    68 
    69 }
    70 
    71 void Assistant::introduce()
    72 {
    73     cout<<"Assistant "<<name<<" teach "<<teach_class<<" and learning "<<learn_class<<endl;
    74 }
    75 
    76 
    77 int main()
    78 {
    79     Teacher A("隔壁老王","C++");
    80     Student B("当当","C++");
    81     Assistant C("丁丁","C++","C++ Primer");
    82     A.introduce();
    83     B.introduce();
    84     C.introduce();
    85     return 0;
    86 }

    运行结果

    有四个类,Person,Teacher,Student,Assistant(助教),其中Person是虚基类,Teacher和Student虚继承与Person,Assistant多继承于 Teacher和Student

    如果不实用虚继承,Assistant在继承Teacher和Student时因为这两个类都有name(继承于Person),这样会产生二义性(Ambiguous)错误:

    如果不用虚继承避免二义性,就要明确的使用作用域符号,比如在例子中的name变为Student::name或者Teacher::name,对与方法也是一样处理

    对于这个代码,多继承和虚继承的不同指出在于:1 Teacher和Student 是否virtual 继承Person 2. Assistant 构造函数后调用了Person的构造函数

    三十 三十一  错误处理和调试

    1. 培养并保持一种编译风格

    2. 认真对待编译器给出的错误警告信息

    3. 三思后行 画出代码流程图

    4. 注意检查最基本的语法

    5. 把有问题的代码改成注释

    6. 换一个开发工具试试

    7. 检查是否把所有必要的头文件include进来

    8. 留意变量的作用域和命名空间

    9. 使用调试工具

    10. 把调试好的程序保存起来,然后把代码分成各个模块

    避免错误的技巧 

    1. 利用断言函数在某个程序里的关键代码不成立的时候停止改程序的执行并报错,从而避免发生更严重的问题

     #include <cassert>

    assert()

     2. 利用cout

    三十二  异常

    三十三  动态内存管理 动态数组

    int *p = new int

    delete p

    p = NULL

    将P赋予NULL后,将不会指向任何东西

    用delete释放内存后,指针里会保存一个毫无意义的地址,所以要将指针赋值NULL

    new语句返回的内存块很可能充满垃圾数据,所以通常先往里面写一些东西覆盖,再访问它,或者直接写一个构造器再初始化

    使用new语句必须有匹配的delete语句

    delete语句只释放给定指针变量正指向的内存块,不影响这个指针。在执行delete语句后那个内存块被释放了,但指针变量依然还在

     1 #include <iostream>
     2 
     3 using namespace std;
     4 
     5 int main()
     6 {
     7     int count;
     8     cout<<"Please input the array count:"<<endl;
     9     cin>>count;
    10     int *p = new int [count];
    11     for(int i = 0;i < count;i++)
    12     {
    13         *(p+i) = i;
    14         cout<<"["<<i<<"] = "<<i<<endl;
    15     }
    16     delete p;   //别忘了着两行
    17     p = NULL;
    18     
    19     return 0;
    20 }

    三十五  从函数或方法返回内存

    注意区分函数指针和指针函数  指针函数:返回值指向够格地址单元  函数指针:一个指向函数首地址的指针变量

    指针函数

    从函数或方法返回内存是指让函数申请并返回一个指向内存块的指针,其基本思路是:函数里调用new语句为一个对象或某种基本类型的数据分配一块内存,再把那块内存地址返回给程序的主代码,主代码将使用那块内存并在delete后释放

     1 #include <iostream>
     2 
     3 using namespace std;
     4 
     5 int *point(int);
     6 
     7 int main()
     8 {
     9     int *p = point(22);
    10     cout<<*p<<endl;
    11     delete p;
    12     p = NULL;
    13 
    14     return 0;
    15 }
    16 
    17 int *point(int value)
    18 {
    19     int *p = new int;
    20     *p = value;
    21     return p;
    22 }

    注意:

    不能让函数返回一个指向局部变量的指针,因为局部变量在栈里,函数结束后后立即释放

    利用指针在某个函数内部改变另一个函数局部变量的值:swap(*a,*b)

    函数指针

    1. 定义

    每一个函数都占用一段内存单元,它们有一个起始地址,指向函数入口地址的指针称为函数指针。

    2. 语法

    指向函数的指针变量的一般定义形式为:

    数据类型 (*指针变量名)(参数表);

    3. 说明

    1) 函数指针的定义形式中的数据类型是指函数的返回值的类型。

    2) 区分下面两个语句:

    int (*p)(int a, int b); //p是一个指向函数的指针变量,所指函数的返回值类型为整型

    int *p(int a, int b); //p是函数名,此函数的返回值类型为整型指针

    3) 指向函数的指针变量不是固定指向哪一个函数的,而只是表示定义了一个这样类型的变量,它是专门用来存放函数的入口地址的;在程序中把哪一个函数的地址赋给它,它就指向哪一个函数。

    4) 在给函数指针变量赋值时,只需给出函数名,而不必给出参数。

    如函数max的原型为:int max(int x, int y); 指针p的定义为:int (*p)(int a, int b); 则p = max;的作用是将函数max的入口地址赋给指针变量p。这时,p就是指向函数max的指针变量,也就是p和max都指向

    函数的开头。

    5) 在一个程序中,指针变量p可以先后指向不同的函数,但一个函数不能赋给一个不一致的函数指针(即不能让一个函数指针指向与其类型不一致的函数)。

    如有如下的函数:int fn1(int x, int y); int fn2(int x);

    定义如下的函数指针:int (*p1)(int a, int b); int (*p2)(int a);

    p1 = fn1; //正确

    p2 = fn2; //正确

    p1 = fn2; //产生编译错误

    6) 定义了一个函数指针并让它指向了一个函数后,对函数的调用可以通过函数名调用,也可以通过函数指针调用(即用指向函数的指针变量调用)。

    如语句:c = (*p)(a, b); //表示调用由p指向的函数(max),实参为a,b,函数调用结束后得到的函数值赋给c。

    7) 函数指针只能指向函数的入口处,而不可能指向函数中间的某一条指令。不能用*(p+1)来表示函数的下一条指令。

    8) 函数指针变量常用的用途之一是把指针作为参数传递到其他函数。

    三十六  拷贝构造函数(复制构造函数,副本构造器)

    可以把一个对象赋值给一个类型与之相同的变量,编译器将生成必要的代码把”源”对象各属性的值分别赋值给”目标”对象的对应成员,这种赋值行为称之为逐位复制(bitwise coyp)

    这种行为在绝大多数场合都没有问题,但如果某些成员变量是指针的话,问题就来了:对象成员进行逐位复制的结果是你将拥有两个一摸一样的实例,而这两个副本里的同名指针会指向相同的地址,当删除其中一个对象时,它包含的指针也将被删除,但万一此时另一个副本(对象)还在引用这个指针,就会出问题

    解决的方式是对操作符进行重载,我们将重载”=”操作符,在其中对指针进行处理:

    MyClass &operator = (const MyClass &rhs);

    因为这里使用的参数是一个引用,所以编译器在传递输入参数时就不会再为它创建另外一个副本(否则可能导致无限递归)

    又因为这里只需要读取这个输入参数,而不用改变它的值,所以我们用const把那个引用声明为一个常量确保万无一失。

    返回一个引用,该引用指向一个MyClass类的对象。如果看过我们待会实现的源码,可能会发觉这个没有必要。但是,这样确实是一个好习惯!

    改写下测试代码:
    MyClass obj1;
    MyClass obj2 = obj1;

    这与刚才那三行的区别很细微,刚才是先创建两个对象,然后再把obj1赋值给obj2。
    现在是先创建一个实例obj1,然后再创建实例obj2的同时用obj1的值对它进行初始化。
    虽然看起来好像一样,但编译器却生成完全不同的代码:编译器将在MyClass类里寻找一个副本构造器(copy constructor),如果找不到,它会自行创建一个。

    即时我们对赋值操作符进行了重载,由编译器创建的副本构造器仍以”逐位复制”方式把obj1赋值给obj2。
    换句话说,如果遇到上面这样的代码,即时已经在这个类里重载了赋值操作符,暗藏着隐患的”逐位复制”行为还是会发生。

    想要躲开这个隐患,还需要亲自定义一个副本构造器,而不是让系统帮我们生成。
    MyClass( const MyClass &rhs);

    更多参见:拷贝构造函数(复制构造函数)

    三十七  高级强制类型转化

    dynamic_cast<>()

    三十八  避免内存泄漏

    函数调用结束后p指针会消失,但是new的内存会一直存在,导致new的内存位置丢失

    1 void fun()
    2 {
    3     Student *p;
    4     p = new Student;
    5 }

    方法一:

    1 void fun()
    2 {
    3     Student *p;
    4     p = new Student;
    5     delete p;
    6     p = NULL;
    7 }

    方法二:

    1 Student *fun()
    2 {
    3     Student *p;
    4     p = new Student;
    5     return p;
    6 }

    注意: 

    动态内存不存在作用域的问题,一旦被分配,内存块就可以在程序的任何地方使用

    以为动态内存没有作用域,所以必须由程序员来跟踪他们的使用情况,并在不再需要用到他们的时候把他们及时归还给系统

    虽然动态内存没有作用域,但是用来保存其地址的指针变量是有作用域的

    三十九 四十 四十一  命名空间和模块化编程

    头文件 系统头文件放在<> 自定义头文件放在“”

    可以用头文件来保存程序任何一段代码,如函数或类的声明,但一定不要用头文件夹保存它们的实现

    头文件里应该使用更多的注释

    如果没有给出路径名,编译器将到当前子目录以及当前开发环境中的其他逻辑子目录里去寻找头文件

    创建命名空间

    namespace ***

    {

    }

    命名空间后面没有分号

    使用命名空间的三种方法:以cout为例

    一:using namespace std;  这样std中所有都可以使用,但是失去了命名空间的意义

    二: std::cout<<      每次使用cout都需要加std::

    三: using std::cout;     一次声明

    四十二 四十三  链接和作用域

    每个源文件都被称为一个翻译单元(translation unit),在某一个翻译单元里定义的东西在另一个翻译单元里使用正是链接发挥作用的地方。

    作用域、链接和存储类是相互关联的概念,它们有许多共同的术语,只是观察和描述问题的角度不同罢了。

    存储类(storage class)

    每个变量都有一个存储类,它决定着程序将把变量的值存储在计算机上的神马地方、如何存储,以及变量应该有着怎样的作用域。

    默认的存储类是auto(自动),自动变量存储在称为栈(stack)的临时内存里并有着最小的作用域,当程序执行到语句块或函数末尾的右花括号时,它们将被系统回收(栈回收),不复存在。

    与auto不同的是static,static变量在程序的生命期内将一直保有它的值而不会消亡,因为它们是存储在静态存储区,生命周期为从申请到程序退出(和全局变量一样)。

    第三种存储类是extern,它在有多个翻译单元时非常重要。这个关键字用来把另一个翻译单元里的某个变量声明为本翻译单元里的一个同名全局变量。注意,编译器不会为extern变量分配内存,因为在其他地方已经为它分配过内存。用extern关键字相当于告诉编译器:“请相信我,我发誓我知道这个变量在其他翻译单元里肯定存在,它只是没在这个文件里声明而已!”

    还有一个存储类是register,它要求编译器把一个变量存储在CPU的寄存器里。但有着与自动变量相同的作用域。register变量存储速度最快,但有些编译器可能不允许使用这类变量。

    在使用编译器建议程序时,它实际上是由3个步骤构成:

    执行预处理器指令

    把.cpp文件编译成.o文件

    把.o文件链接成一个可执行文件

    链接分为三种情况,凡是有名字的东西(函数、类、常量、变量、模板、命名空间,等等)必然属于其中之一:外连接(external),内链接(internal)和无链接(none)。

    外链接的意思是每个翻译单元都可以访问这个东西,需用extern

    内链接的含义是:在某个翻译单元里定义的东西只能在翻译单元里使用,在任何函数以外定义的静态变量都有内链接 static

    在函数里定义的变量只存在于该函数的内部,根本没有任何链接(none)

    四十四  函数模板

     1 #include <iostream>
     2 
     3 using namespace std;
     4 
     5 template <class T>
     6 
     7 void myswap(T &a,T &b)
     8 {
     9     T temp;
    10     temp = a;
    11     a = b;
    12     b = temp;
    13 }
    14 
    15 int main()
    16 {
    17     string x = "abc",y = "cba";
    18     cout<<x<<y<<endl;
    19     myswap(x,y);
    20     cout<<x<<y<<endl;
    21     int a=2,b=3;
    22     cout<<a<<b<<endl;
    23     myswap(a,b);
    24     cout<<a<<b<<endl;
    25 
    26     return 0;
    27 }

    本来以为很简单,但在编译过程中发现两个问题:

    1. 写的是交换函数,函数名不要用swap,因为codeblocks的后端gcc/mingw也支持模板,所以要换一个文件名

    2. 若是将模板函数的实现放在main后面,那还要再定义函数前再加上template <class T>,否则不能编译通过,所以不要把模板函数的定义和声明分开,要放到最前面

    四十五  类模板

    四十六 内联模板

    四十七  四十八  容器和算法

     1 #include <iostream>
     2 #include <vector>
     3 
     4 using namespace std;
     5 
     6 int main()
     7 {
     8     vector<string> names;
     9     names.push_back("老王");
    10     names.push_back("老李");
    11     names.push_back("老张");
    12     names.push_back("老赵");
    13     names.push_back("老潘");
    14     cout<<names.size()<<endl;
    15 
    16     for(int i = 0; i<names.size(); i++)
    17         cout<<names[i]<<endl;
    18     cout<<"下面用迭代器"<<endl;
    19     vector<string>::iterator iter = names.begin();
    20 
    21     while( iter != names.end())
    22     {
    23         cout<<*iter<<endl;
    24         ++iter;
    25     }
    26 
    27     return 0;
    28 }
  • 相关阅读:
    Nginx配置文件nginx.conf中文详解
    Linux安装nginx
    熊猫TV游戏直播教程-OBS篇
    Mac下MySQL卸载方法
    sphinx 1.10-实时索引 api
    freebsd 国内相当快的ports源地址
    Springboot框架中如何读取位于resource资源中的properties配置文件,并将配置文件中的键对应的值赋值到目标bean中?
    分析Jedis源码实现操作非关系型数据库Redis
    分析线程池源码测试线程池
    socket简单示例实现从服务器拷贝文件到客户端
  • 原文地址:https://www.cnblogs.com/raichen/p/4735600.html
Copyright © 2020-2023  润新知