C++基础

C++基本知识

1. 声明文件与定义文件编写规范

#ifndef __NAME__
#define __NAME__

......
    
#endif
  • 一个.h文件需要对应至少一个.cpp文件。

  • .cpp定义(define)与.h声明(declare)分开,这是一种C++编程范式。注意这种范式需要注意以下这种情况:

    //声明
    //a.h
    void f();
    int global;
    
    //定义
    //a.cpp
    #include"a.h"
    //b.cpp
    #include"a.h"
    

    编译链接之后

    /usr/bin/ld: b.o:(.bss+0x0): multiple definition of `global'; a.o:(.bss+0x0): first defined here
    /usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/8/../../../x86_64-linux-gnu/crt1.o: in function `_start':
    

    在a.h文件当中,f()函数是声明,而global变量是定义;因此在编译过程当中并没有问题,问题出在链接过程当中,链接器告诉我们:a.cpp与b.cpp都有global函数,而且都已经被定义出现重名。

    要解决这个问题,只需要在a.h当中把global加入extern改成声明

    //新声明
    //a.h
    void f();
    extern int global;
    
    //定义文件
    //a.cpp
    #include"a.h"
    int main()
    {
        return 0;
    }
    //b.cpp
    #include"a.h"
    

    编译没有出错。

    在头文件当中的变量前加入extern使得变量成声明而非定义

2. 构造函数编写规范

2.1 初始化问题

class complex
{
    private:
    double re,im;
    public:
    complex (double r=0,double i=0):re(r),im(i)  //一般赋初值在这里而最好不要在body里头 
    {
        
    }
}

2.2 Default value

  • 首先,参数有默认值的构造函数以及 无参数构造函数两者不能同时存在,即重载不能这里起作用。当创建一个没有参数的对象时,程序不知道该用哪一个构造函数。

    complex(double r=0,double i=0)
    {
        
    }
    //不能和以下同时存在
    complex()
    {
        
    }
    
  • 构造函数或者其他函数参数有默认值则必须从右往左,默认参数不能只出现在中间或者左边

    A(int a=0,int b=1,int c)//error!
    B(int a,int b,int c=1)  //right!
    C(int a,int b=1,int c)  //error!
    
  • 有默认值参数的函数只出现在.h文件当中,而在具体实现.cpp文件当中则不能再次出现默认值

    //.h文件
    int A(int a=1,int b=2,int c=3);
    //.cpp文件
    int A(int a,int b ,int c)
    {
        ......
    }
    
  • 缺省构造函数并不是指编译器内部的构造函数,而是需要我们给出没有参数的构造函数;因此,我们应该显式给出无参构造。

  • 对于继承问题中,子类继承父类所有函数包括构造函数。当父类只给出有参构造,而子类要进行无参构造。则在子类的构造函数当中必须显式调用父类的构造函数。

    class A
    {
        private:
        int age;
        public:
        A(int a):age(a)
        {
             std::cout<<"start"<<std::endl;
        }
    };
    class B:public A
    {
        B():A(10)    //显式调用A的构造函数。
        {
            
        }
    };
    

3. 参数传递与返回值

3.1 非写入函数const必要性分析

class complex
{
  private:
    double re,im;
  public:
    complex (double r=0,double i=0):re(r),im(i)  //一般赋初值在这里而最好不要在body里头 
    {   
    }
    double real()const{return re;}//1
    double img()const{return im;} //2
     
}
/*
在1,2 函数处我们在非写入函数前加入const,用来声明该函数不会修改数据防止。如果不加的话,可能程序不会出错但是编译器会出错。如出现以下这种情况
*/
{
   const complex c1(1,2);
   c1.real();
   c2.img();
}
//在声明complex对象的时候,声明为const表明我的初始化不能改变,如果1,2没有加const则表明1,2可能会改变;使得编译器会报错。

3.2 参数传递

参数传递分为传递值与传递引用。由于传递值受值本身大小的影响,传递速度会很慢;因此推荐传递引用。但是如果不希望函数更改数据,可以在最前面加上const,如下所述:

complex& operator +=(cosnt complex&);

3.3 返回值传递

尽量传递引用(前提是可以传递引用的情况下)

如果要求返回一个引用,则不能传递函数体中创建的变量,一般传递的是传进来的参数。

3.4 友元函数

可以直接访问对象数据。

对于以下情况

class complex
{
  public:
    complex(double r=0,double i=0):re(r),im(i)
    {
        int func(const complex& param){return param.re+param.im;}  //*
    }
  private:
    double re,im  
}
//之所以func函数能够直接通过.来访问变量,是因为:相同class的各个objects互为友元。

4. 成员变量与成员函数

4.1 成员变量存在的空间

//a.h
class A{
 private:
    int i;
 public:
    void f();
}
//a.cpp
#include"a.h"
void A::f()
{
    i++;
}
int main()
    
{
    A a;
    a.f();
}

在类A的声明当中我们定义了一个私有变量i,这个变量仅仅是声明,在内存当中是不存在的;因此只有当类具有对象时例如对象a此时i才确确实实存在,因此类中的变量存在于各个具体化的对象当中。

4.2 成员函数

成员函数是属于类的,不是属于对象的。因此上述中a.f()函数是属于A这个类的而非对象a。

// 在C++中
a.f();
//可以在C语言当中被翻译成:
A::f(&a);
//即,将a的地址传给f函数,让它替我执行操作。

因此在成员函数当中可以使用this指针代替对象,来直接引用变量即:

//在类A当中,可以这样写
class A{
 private:
    int i;
 public:
    void f();
}
void A::f()
{
     this.i=10;//代替i=10;
}

4.3 成员变量的初始化

成员变量最好在initialization中初始化,而不要在构造函数中进行初始化

class A{
    private:
      int i;
    A(int s):i(s)  //在这进行初始化
    {
                   //而最好不要在这进行初始化
    }
}

5. 动态内存分配:new、delete

5.1 new申请空间并返回空间地址

new int ;
new int [10];
new 对象;
int *p=new int [10];

5.2 delete

delete [] p;
delete a;

tips:

  1. 两者是配套使用的,即用new申请的空间,必须使用delete来释放

  2. new申请空间并不是向操作系统申请,而是在分配给整个项目的空间中进行分配,即在堆中进行分配。

  3. 不要用delete释放不是用new申请的空间

  4. delete可以用来释放空指针(即使该指针并不是用new产生的)

6. 访问限制

6.1 public

在该区域的变量,能够被除了自己,别的函数也能访问

6.2 private

只能自己访问(这个自己指的是成员函数),

  1. 该成员函数指的是在class之中的函数例如:
class A{
       private:
          int a;
       public:
          void set_a(int i);   //成员函数
   }
int index()                  //非成员函数
{
   
}

  1. 同一个类的不同对象可以互相访问对方的私有变量
class{
   private:
      int i;
   public:
      void set(int a){this.i=a};
      void display(A &b){cout<<"i="<<b.i};
}
int main()
{
   A a;
   a.set(10);
   A b;
   b.display(a);
}

6.3 protected

继承,即只有这个类的子子孙孙才能访问

6.4 friends

使得非成员函数或者结构或者变量也能访问本类的private对象

class A{
   private:
      int a;
   public:
      void f();
   friend h();     //函数
   friend struct X;//结构
}

即friend函数一定不是成员函数

7. 继承

7.1 含义

点击链接

7.2 继承关系需要注意的地方

  1. 同名屏蔽

    当父类重载了许多函数例如构造函数,子类又给出同名的构造函数或许其他函数,此时父类的所有跟子类同名的函数都会被屏蔽,只留下子类自己的函数

    class A{
        void print(){};
        void print(int a){};
    }
    class B:public A{
        void print();
    }
    int main()
    {
        B b();
        b(10);   //此时调用会报错,因为父类的print(int a)被屏蔽。
    }
    

8. const

8.1 基本含义

Person p1("zengwei");
Person *const p=&p1;//指针是const,即p不能执行p++操作,但是对象可以改变
const Person *p=&p1;//对象是const,意思是不能通过p指针去修改对象,言外之意就是如果p1被其他指针修改了也                       是可行的。
Person const *p=&p1;//同上

某个变量是const是在编译时刻才有作用的,由编译器保证不修改。在运行时刻是完全不知道的。

int i;const int ci=3;
int *ip;ip=&i;ip=&ci(error!);//由于*ip非const
const int *cip;cip=&i;cip=&ci;
  • 如果在对象前面加上const

    const Person person("zengwei");
    

    则该对象里面的变量都不能修改。因此最好不要这么做。

  • 为了保证某个函数不能修改变量,我们可以在函数的原型和定义的地方后面加上const

     int Person::Getdata() const
     {
         .....
     }
    

    在函数后面加入const保证该函数不会修改任何变量。且注意,

    此处的const相当于const Person *this;即对象在此处是const。因此函数可改写为:

    int Person::Getdata(const Person* this )
    {
       ......
    }
    

    基于此,以下重载是正确的

    class Person
    {
       void f();  //==void f(Person *this);
       void f()const;//==void f(const Person *this);
    }
    
    

    因为参数表不同。

  • 如果成员变量是const,则必须在initialization里进行初始化

    class Person{
        private :
          const int i;
        public:
          Person()i(0){....};
    }
    
  • 如果成员变量是const,则不能用该变量用作数组大小。

    class Person{
        private:
          const int i;
          int array[i];//error!
        public:
    }
    

9. 引用

引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字。一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量。

  • 注意点
    1. 引用创建时必须赋初始值
    2. 引用之间不能互相赋值
    3. 引用不可以为空(Null)
    4. 一旦引用某一对象就不能再将其作为其他对象的引用
    5. 当函数需要一个返回引用的时候不能返回该函数的临时变量

引用就是指针

  • 如果定义类的时候,需要定义一个引用

    class A{
        int &m_y;
    }
    

    由于无法在里面直接初始化,那该如何做呢?

    在构造里面的initialization里面对引用进行初始化

    class A
    {
       private:
       int &m_y;
       public:
       A(int b):m_y(b){....}
    }
    

    因为只有在initialization里面才是初始化,在任何函数体内部都是赋值。(赋值和初始化不同!)

  • 如果函数返回一个引用,则该函数可以用来作为左值使用,同时也可以用来作为右值

    int& func(int a)
    {
        ....
            return a;
    }
    int main()
    {
        func(a)=12;          //等价于a=12
        int get_data=func(a);//等价于get_data=a;
    }
    
  • 函数的参数如果是一个表达式,在内部会产生一个临时变量来存储计算的表达式。因此以下用法请注意

    void func(int &);
    func(i*3);//该用法是错误的,因为内部产生的临时变量const int temporary=i*3;而func接受一个非               const的变量,因此会报错。
    void func(const int &);
    func(i*3);//该用法正确,因为在声明func的时候明确接受一个const即表达不会修改该参数,可以放心把参数             给我了
    
    

10. 向上造型

向上造型是子类与父类之间的动作。

class A
{
    private:
    int a;
    string name;
    public:
    A();
    void func_a();
}

class B:public A
{
    private:
    int b;
    public:
    B();
    void func_b();
}
int main()
{
    A a;
    B b;
    A aa=b;   //向上造型
    A &aaa=b; //向上造型
    return 0;
}

向上造型的结果是:aa、aaa看到的只是A中的声明的变量,看不到B中的变量

11. 多态

多态与上节讲的向上造型有很大的关系,同时也和虚函数==(virtual)==有很大关系。同时也涉及函数的动态绑定与静态绑定。

所有含有虚函数的类对象,比没有虚函数对象多一些东西。

一般的类对象:
在这里插入图片描述

含有虚函数的类对象:

在这里插入图片描述

含有虚函数对象当中会有一部分空间用来存储vptr,它是一个指针,指向虚函数表vtable

假设有类A

class A
{
    private:
       int i;
    public:
      virtual func_1();
      virtual func_2();
}
class B:public A
{
    private:
     int j;
    public:
    virtual func_1()
    {
        
    }
    virtual func_2()
    {
        
    }
      virtual func_3();
}
int main()
{
    B b;
}

则对应的结构为:
在这里插入图片描述

访问虚函数的流程是:

找到vtable 然后直接寻址找到对应的虚函数。

  • 基于以上认识,只有在用指针进行引用虚函数的时候,才能保证虚函数是动态绑定即:
int main()
{
    A a;
    B b;
    A* p=(int *)&a;
    a=b;
    a.func_1();
}
//在a.func_1()此处执行的仍然是a的func_1();因为a=b只是赋值并没有改变地址。
int main()
{
    A a;
    B b;
    A* p=&a;
    int* r=(int *)&a;
    int* c=(int *)&b;
    *r=*c;
    p->func_1();
}
//p->func_1()执行的是b的func_1(),因为地址改变了
  • 同时也是基于以上认识,当某个类可能被继承时,我们应该把这个类的析构函数设置成虚函数(即动态绑定)

    class A
    {
        private:
        int a;
        
        public:
        virtual A();
    }
    class B:public A{
        
    }
    int main()
    {
        A *p=new B();
        ....
        delete p;    //如果基类构造函数是虚函数,则在delete p的时候会动态绑定B的析构函数,而非A类的。
    }
    
  • 如果子类overriding 父类一个虚函数(overriding 只能谈虚函数其他函数不能被override),且该函数需要在子类同名的函数当中用到,则在调用的时候需要加上父类的作用域:

    class A
    {
        private:
        int a;
        
        public:
        virtual A();
        void func();
    }
    class B:public A{
        void func(){
            A::func();   //
        }
    }
    
  • 如果父类对对某个虚函数进行的重载,则子类覆盖了该虚函数,则子类必须同时覆盖他的重载函数

    class A
    {
        private:
        int a;
        
        public:
        virtual A();
        virtual void func_1();
        virtual void func_1(int a);
    }
    class B:public A{
        public:
        void func_1()override;
        void func_1(int a)override;
    }
    

12. 拷贝构造(复制构造)

区分初始化与赋值运算

A a=aa;  //定义时给出值===》初始化
a=aa;    //已经定义好了,之后再赋值==》赋值运算
//初始化只能做一次,赋值可以做无数次

拷贝构造是一种初始化,拷贝构造函数与构造函数一样,如果没有显式给出,系统会补上。

12.1 为什么需要拷贝构造函数

当出现以下情况:

class A
{
    private :
    int index;
    public:
    A(){cout<<"no para"<<endl;}
    A(int a):index(a){cout<<"para"<<endl;}
   
}
A  f(A a){cout<<"A:print"<<endl;return a}
//第一种情况:
int main()
{
    A a;
    A aa=10;//*
}
//第二种情况:
int main()
{
    A a;
    A aa=f(a);//**
}

*处是 将10赋值给对象aa,这是正确的做法,因为对于类A存在有参构造,实现方式是直接把10赋值给index。

//实现代码
#include<iostream>
static int count=0;
class A
{
  private:
    int index;
  public:
   A(){
 count++;
 std::cout<<"count:"<<count<<"  no paramtype"<<std::endl;}
   A(int a):index(a){
 count++;
 std::cout<<"count:"<<count<<"  paratype"<<std::endl;
}
   ~A(){
 count--;
 std::cout<<"count:"<<count<<"  destructure...."<<std::endl;
}
};
A func(A a)
{
 std::cout<<"func....."<<std::endl;
 return a;
}
int main()
{
  A a=10;
  return 0;
}

**处由于不存在带有对象参数的显示构造函数,因此会用默认的构造函数进行构造,但是析构的时候依然使用显式析构函数。但会出现异常。例子:

#include<iostream>
static int count=0;
class A
{
  private:
    int index;
  public:
   A(){
 count++;
 std::cout<<"count:"<<count<<"  no paramtype"<<std::endl;}
   A(int a):index(a){
 count++;
 std::cout<<"count:"<<count<<"  paratype"<<std::endl;
}
   ~A(){
 count--;
 std::cout<<"count:"<<count<<"  destructure...."<<std::endl;
}
};
A func(A c)
{
 std::cout<<"func....."<<std::endl;
 return a;
}
int main()
{
  A a(10);
  A b=func(a);
  return 0;
}
//输出:
count:1  paratype
func.....
count:0  destructure....
count:-1  destructure....
count:-2  destructure....
//说明还有两个临时对象在func函数里面创建了,使用的是默认构造函数。
//首先b的构造函数肯定是默认构造函数,其次在函数体func参数中也使用默认构造函数构造临时对象a。

基于上述情况,我们需要显式给出拷贝构造函数。这样做才是安全的。

T::T(const T&){
    .....
}
//下面讨论...里面应该写什么东西。

12.2 如何实现拷贝构造函数

试想:假如对象有指针,如果使用默认构造函数的话,两个对象的指针指向的是同一个地方,当其中一个对象释放指针的时候另外一个对象并不准备释放,结果就会出错。因此,在显式构造函数里面我们必须考虑这种情况。这也是为什么需要给出显式构造函数。

因此,我们应该在拷贝构造函数里面重新申请一个空间供新对象使用

class A
{
    private:
      char* name;  //内部使用指针实现的
    
    public:
    A();
    A(const A&);
}
A::A(const A& w)
{
    name=new char[std::strlen(w.name)+1];  //+1 指的是结束符:\0
    std::strcpy(name,w.name);
}

12.3 何时会调用拷贝构造函数

  • 当以类本身(指本身不是引用也不是指针)为参数的函数,如12.1节所示
  • 当返回的是对象本身的时候

阶段总结

当我们写完一个类,我们必须手动加上几个东西:

  1. 默认构造函数(无参构造)

  2. 虚析构函数

  3. 拷贝构造函数

    如果不希望对象被拷贝,可以将拷贝构造函数设置成private,但是设置成private,会使得不能用该对象构造其他对象,同时也不能进行其他操作,因此不建议这么做。

13.析构函数

  • 作用

  • 调用时机

14. static

  • static在C++中有两层意思:

    1. 被隐藏了,不被其他文件所看见(相对于全局变量)
    2. 永久存储(与运行时间一致)
  • 在C++中,如果某个类存在静态的成员变量,则该成员变量对所有对象是可见的,而且是唯一的。但是如果要在类对象当中加入静态成员变量,我们需要在类外进行定义。因为在class里面的东西都是声明

    class A
    {
        private:
          static int i;   //静态成员变量
        
        public:
          A();
        void print();
    }
    int A::i    //在此处进行定义
    int main()
    {
        
    }
    
    1. 对静态成员变量初始化需要在定义的地方,而在构造函数的initialization不能对其进行初始化,但是可以在构造函数内部进行初始化。

    2. this指针可以访问静态成员变量。即:this->i

  • static也可以用来修饰函数,叫做静态成员函数。对于静态成员函数的访问

    class A
    {
        private:
          static int i;
        public:
          A();
          static void print(){cout<<i;}
    }
    
    int main()
    {
        A a;
        a.print();   //第一种方式
        A::print();  //第二种方式
        return 0;
    }
    

    静态的成员函数只能够访问静态的成员变量

    静态的成员函数体内部不能出现this,包括this->静态成员变量。该条规则保证在没有对象的情况下依然能够使用。

15. 拷贝赋值

假设两个对象原本都有数据,当我们需要将其中一个对象的内容赋值给另外一个对象的时候,我们就需要拷贝赋值函数。

  • 拷贝赋值的过程

    假设对象A需要赋值给对象B;首先,我们需要将B内容清空(空间也没了),然后重新申请内存,最后将A的内容赋值给B。总结的代码如下:

    class A or B
    {
      private:
        m_data;  
        .....
      public:
    //拷贝赋值函数
       String& String::operator=(const String& str)
        {
          if(this==&str)return *this  //检查是否是自我赋值,很必要!!!
          delete[] m_data;
          m_data=new char[strlen(str.m_data)+1];
          strcpy(m_data,str.m_data);
          return *this
        }
    }
    
    //示例
    int main()
    {
        class A,B;
        B=A;
        return 0
    }
    //注:上面的检查是否自我赋值必要性如下:假设对象A,B的指针m_data指向相同内存,在B进行到delete[]m_data时,那块内存被释放了,接下来的赋值操作就进行不下去了。
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值