基础知识复习

    1. 无参宏

    2. 有参宏:有参宏中使用自增运算符时,导致逻辑上的错误,且无法避免

    1
    2
    3
    4
    5
    6
    7
    #define MAX(a,b) a>b?a:b
    int main(){
    int n1 = 1, n2 = 0;
    int n3 = MAX(n1, n2);//n3 = 1
    int n4 = MAX(n1++, n2++);//n4=2,n1=3,n2=1,替换后再运算

    }
  • 内联函数 inline

    成为内联函数标准:

    1. 函数体足够小(代码行数少)
    2. 函数内部不能有复杂的逻辑,
    3. 用法和函数一样
    4. 功能和有参宏差不多
    1
    2
    3
    4
    5
    6
    7
    8
    9
    inline int max(int a, int b){
    return a > b ? a : b;
    }
    int main(){
    int n1 = 1, n2 = 2;
    //编译后这里的函数调用会替换成函数体,类似有参宏在预编译后直接替换
    //但实参是表达式时,会先计算出表达式值,再替换(而有参宏是直接拿表达式来替换)
    int n3 = max(n1++, n2++);
    }
  • const

    const 是常量修饰符:左定值,右定向

    1. 常量必须被初始化

    2. 使用常量时,编译器会直接将常量名用它的初始值来替换(和无参宏一样,在原码中使用宏名时,经预处理后会被替换成宏的内容)

    3. 使用场合

      1. 修饰指针

        定义指针时,*const 左边,指针是一个指针常量;*const 右边,指针是一个常量指针

        • 常量指针:将指针指向的内容修饰为常量

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          char szBuff[100] = {"hello"};
          szBuff[0] = 'A';//可以修改
          //p就是常量指针
          //1.指向的内容被修饰为常量,不能通过p去修改内容
          //2.p自身可被修改
          //3.const只是在语法层面上限制一个指针不能修改它指向的内容
          // 至于指针指向的内容能不能被修改是不受const的拘束
          const char* p;//可以不用初始化
          p = szBuff;
          const char* p1 = "world";
          szBuff[0] = 'B';//仍然可以被修改
          p[0] = 'A';//语法报错
          p1[0] = 'A';//语法报错
   - 指针常量:将指针自身修饰为常量

     
1
2
3
4
5
6
7
8
9
char szBuff[100] = {"hello"};
//定义一个指针常量必须要被初始化
char* p const = szBuff;
//指针不能再指向其他内存
p = "hello";//错误,p只能指向szBuff的值
p[0] = 'A';//语法上可以修改,正确,因为修改的是数组元素
char* p2 const = "world";
p2[0] = 'A';//语法上是可通过的,运行时错误,p2保存的是常量字符串的地址,常量字符串
//不能修改
2. 修饰引用
1
2
3
4
5
6
7
8
9
int n = 100;
//定义一个引用
int& rNum = n;
rNum = 10;//实际上修改的是变量n

//定义一个常量引用
const int& rNum2 = n;
rNum2 = 20;//报语法错误,不能修改
n = 20;//正确,可以修改
3. 修饰成员函数:实际修饰的是 `this` 指针,也就是将 `this` 指针修饰成常量的指针
1
2
3
4
5
6
7
8
9
10
11
12
13
class MyClass{
int m_nNUm;
public:
void fun(){
m_nNum = 100;
}
};
void fun(const MyClass* pObj){
pObj->fun();//语法报错
//pObj是一个常量指针
//成员函数有条件修改自身的成员变量,这样相当于通过指针调用了成员函数,成员函数在内部
//修改了成员,实际上相当于间接通过指针修改了指针指向的内存,这是常量指针不允许的
}
修改:
1
2
3
4
5
6
7
8
9
10
11
12
class MyClass{
int m_nNum;
public:
// const 修饰的是this指针
// this 就相当于 const MyClass* this
void fun() const {
//this->m_nNum = 100; // 此处会报语法错误,因为m_nNum不可修改
}
};
void fun(const MyClass* pObj){
pObj->fun(); // 不会报错
}
总结: 1. 在成员函数后加 `const`,成员函数就是一个**常量成员函数**,在这样的函数内部不能修改自身的成员变量,也不能调用其他的非常量成员函数 2. 通过**常量对象指针** `const MyClass* pObj` 只能调用常量成员函数
  • 引用

    性质:

    1. 引用类型的变量不占用内存空间,其内存空间来自被引用的变量

      • 定义时必须初始化
      • 不能再引用其他变量
      • 修改自身,被引用的变量也会被修改,被引用的变量修改了,引用类型的变量也会被修改,因为它们共用同一块内存空间
    2. 使用场合

      • 传参

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
         void swap(int& a, int& b){
        int t = a;
        a = b;
        b = t;
        }
        int main()
        {
        int n1 = 10, n2 = 20;
        swap(n1, n2);//执行后,n1 = 20,n2 = 10
        }
- 传递函数返回值

  
1
2
3
4
5
6
7
8
9
10
11
12
13
 int &fun(int &n){
return n;
}
int main()
{
int nNum = 100;
fun(nNum) = 200;//修改了nNum
int n2 = fun(nNum);
n2 = 10;//nNum未被修改,因为n2不是引用类型的变量,它拥有自己的内存空间,
//不会影响nNum
int &rNum = fun(nNum);
rNum = 10;//修改了nNum,因为rNum是一个引用,无独立的内存空间,与nNum共用内存
}
  • 类型转换

    1. const_cast:常量类型转换,去除常量类型

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      void fun(const char* pStr){
      pStr[0] = 'A';//语法报错
      //C语言风格,强转
      char* p = (char*)pStr;
      p[0] = 'A';
      //C++风格
      char* p2 = const_cast<char*>(pStr);//类似于(char*)pStr
      }
      int main()
      {
      char buff[100];
      fun(buff);
      }
  1. static_cast:静态类型转换,编译器认可

    1
    2
    3
    4
    5
    6
    7
    8
    void fun(int n){}
    int main()
    {
    //隐式转换,编译器将double类型的3.14转为int型再传参
    fun(3.14);//语法能通过,但会报一个警告:精度丢失...
    fun((int)3.14);//C
    fun(static_cast<int>(3.14));
    }
  1. reinterpret_cast:强制类型转换,编译器不认可,用于编译器无法进行隐式转换时的类型转换

    1
    2
    3
    4
    5
    6
    int main()
    {
    int* p = 0x403000;//语法报错,类型不匹配
    p = (int*)0x403000;//C
    p = reinterpret_cast<int*>(0x403000);
    }
  1. dynamic_cast:动态类型转换,用于将父类和子类的指针或引用进行转换(继承时会用到,能将基类转换为派生类)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Base{};
    class MyClass : public Base{
    int nNum[100];
    };
    class B : public Base{
    int m_nNum;
    };
    int main()
    {
    Base* obj = new B;//new出来的类型是B*,再转换成Base*
    MyClass* pObj = obj;//语法报错

    //语法上可通过,但运行时会出严重问题
    //因为obj本质上并非MyClass类型
    pObj = (MyClass*)obj;

    //dynamic_cast可检测出是否能够转换,若不能则返回nullptr
    pObj = dynamic_cast<MyClass*>(obj);
    }

面向对象-类

  • 成员变量

    1. 一个对象的数据组成,对象的内存空间就是由成员变量组成;成员在类中的声明顺序决定了成员变量在内存中的顺序

    2. 成员变量的使用

      • 需要提供一个对象才能使用成员变量

      • 在成员函数内,this 指针来表示当前对象

        1
        2
        3
        4
        5
        6
        7
        8
        class MyClass{
        public:
        int m_nNum;
        void fun(){
        //this可省略
        this->m_nNum = 10;
        }
        }
- 在类外部,只能通过对象变量来访问公有成员变量

  
1
2
3
4
5
int main()
{
MyClass obj;
obj.m_num = 20;
}
  • 成员函数

    1. 成员函数内自带一个 this 指针,通过对象调用函数时 c++ 自动将对象内存首地址赋给 this 指针,这样,成员函数被调用后 this 指针就保存着调用了这个成员函数的对象变量

    2. 普通成员函数

      • 需要对象才能调用

      • 通过 this 指针也能调用

      • 其他情况和普通函数一样,可以设置默认参数,也可以进行函数重载

  1. 构造函数

    1.构造函数是一个特殊的成员函数,作用是构造一个对象,无返回值,因为返回值默认就是一个类对象

    2.构造函数自动调用

    • 定义对象:

      1
      2
      3
      4
      5
      MyClass g_obj;//调用构造函数
      int main()
      {
      MyClass obj;//调用构造函数
      }
- 堆空间申请对象:

  
1
2
3
4
int main()
{
MyClass* pObj = new MyClass;//调用构造函数
}
- 函数形参:
1
2
3
4
5
6
void fun(MyClass obj){}
int main()
{
MyClass obj;//调用了构造函数
fun(obj);//会为形参obj调用构造函数.
}
- 函数返回值:
1
2
3
4
5
6
7
8
9
MyClass fun(){
MyClass obj;
return obj;
}
int main()
{
MyClass obj;//调用构造函数
obj = fun();//调用构造函数
}
3.构造函数可以进行重载 - 当构造函数没有进行重载的时候,只能调用无参的构造函数,无参构造函数是 C++ 编译器默认提供的一个构造函数,但如果定义了其它版本的构造函数,编译器就不再提供无参构造 - 在进行构造函数重载后,构造对象时,就可通过传参来决定调用哪个重载版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class MyClass{
public:
MyClass(){}
MyClass(int n){}
MyClass(double d){}
MyClass(int n, double d){}
MyClass(MyClass& obj){}
}
void fun(MyClass obj){}
MyClass fun2(){
return 2;
}
int main()
{
MyClass obj = fun2();//调用转换构造函数,相当于 MyClass obj(2);

//隐式转换:
//c++编译器会尝试将整型实参5转换成MyClass类型的形参,但这不是无依据地转换,规则:
//形参是类类型,且有一个构造函数刚好可将实参传递进去,此时编译器就可将实参传给形参
//的构造函数,直接构造出形参,而不是直接将实参赋给形参
fun(5);//会给形参obj调用构造函数 MyClass(int n)

MyClass obj1(5);//MyClass(int n)
fun(obj1);//给形参obj调用了构造函数 MyClass(MyClass& obj )
MyClass* p = new MyClass(5,1);//MyClass(int n , double d)
MyClass obj2(obj1);//MyClass(MyClass& obj )
}
  1. 构造函数的一些术语:

    • 默认构造:指的是没有形参的构造函数,由编译器默认提供,在某些场合编译器需要自动调用一个类对象的构造函数时,只能调用默认构造,例如:子类继承了父类,当子类对象被构造的时,父类的构造也会被自动调用,此时就只能自动调用父类的默认构造

    • 转换构造:指的是那些只有一个形参,且参数类型是非本类类型的构造函数们,一般能够显式调用(例如 MyClass obj(5)),也能隐式调用: fun(5)

    • 拷贝构造:指的是只有一个形参,且参数类型是本类类型的引用,一般是在定义一个对象时,将另一个对象作为初始值,就会自动调用这个版本的构造函数,一般编译器会默认提供一个拷贝构造,默认提供的拷贝构造会将对象的内存空间进行拷贝

      类中包含有指针成员的时候,一般就需要自己编写拷贝构造,实现对指针指向的内存进行拷贝的功能,而默认拷贝构造是不会去拷贝指针指向的内容(深拷贝和浅拷贝的区别)

    • 带参构造:含有两个以上的形参的构造函数统称带参构造

  2. 析构函数

    作用和构造函数相反,当一个对象被销毁的时候,就会调用析构函数

  3. 运算符重载函数

    除三目运算符、sizeof.:: 运算符外,其他运算符都能重载

    1. 在C++中有很多会被自动调用的代码

      • 构造一个对象, 构造函数被调用了

      • 当一个对象被销毁的时候, 析构函数被调用

      • 当编译器需要进行隐式转换时, 转换构造被调用了.

      • 当一个对象被使用了运算符(+-*/% …)时,运算符重载函数被调用

2. 无论在 C 还是在 C++ 中,能够直接使用运算符进行运算的数据类型一般只有基本数据类型

   - 意思就是,无法直接对一个数组使用 `+`、`-`、`*` 等运算符,结构体变量,类对象同样如此
   - 如果想对非基本数据类型的变量使用运算符,一般需要通过另一些运算符来得到基本数据类型,然后再直接使用运算符,例如一个结构体变量,不能直接使用 `+` 运算符,但是通过 `.` 或 `->` 可以从结构体变量中得到一个字段(基本数据类型),这个基本数据类型就能使用运算符了

3. 在 C++ 中,提供一种特殊的成员函数,这些成员函数的名字都有一个相同的前缀,叫 `operator`,他们一般都有一个不同的后缀,不同的后缀就是运算符,例如:`operator+`, `operator-` 等,这些函数在使用的时候,一般有两种形式:

   - 对象.operator+(5)
   - 对象 + 5

   
1
2
3
string strobj;
strobj.operator+=("123");
strobj += "123";
4. 运算符重载的意义:让子对象直接使用运算符 5. 运算符重载的本质: - 一个运算符就是一个函数调用,若运算符是一个单目运算符,这个函数调用就不需要传递参数,若运算符是双目运算符,那么运算符的左操作数就是对象自身,右操作数就是函数中的形参1 - 当对一个类对象使用运算符时,c++ 编译器在类中查找一个函数(以 `operator` 开头的函数),若没有,就报错,若找到了就调用该函数 调用函数传参的方法: - 单目运算符:不传参,对象自身就是操作数 - 双目运算符: 1. 成员函数版:对象默认为左操作数,参数作为右操作数,如 `obj+5` 会被转换成 `obj.operator+(5)`,而 `5+obj` 就无法转换 2. 友元函数版:形参1为左操作数,形参2为右操作数,如 `obj+5` 转换成 `operator+(obj,5)`,`5+obj` 转换成 `operator+(5,obj)` 6. 运算符重载的使用: - 单目运算符的重载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Number{
int m_nNum = 0;
public:
//前置++
Number& operator++(){
++m_nNum;
return *this;
}
//后置++
Number operator++(int){
//先保存原值
Number t;
t.m_nNum = this->m_nNum;
//自增
++m_nNum;
return t;//返回自增前的值
}
// 前置--
void operator--() {}
// 后置--
void operator--(int) {}

}
int main()
{
Number obj;
Number obj2 = obj++;//转换成运算符重载函数 obj.operator++()
obj.operator++();
//若是后置++,编译器只会找operator++(int)的成员函数
//形参int只是为了区分前置和后置
obj++;//obj.operator++(int)
++++++obj;//obj.operator++().operator++().operator++()
//需调用三次重载函数,故函数的返回值是类的引用
}
- 双目运算符重载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Number{
int m_nNum;
public:
Number():m_nNum(){}
Number(int n):m_nNum(n){}//转换构造函数
Number(const char* pStr){
sscanf_s(pStr, "%d", &m_nNum);
}
//成员函数版,只能在形参中指定右操作数
Number operator+(const Number& n){
//自身拷贝到t
Number t(*this);
t.m_nNum = this->m_nNum + n.m_nNum;
return t;
}
//友元函数版,必须在形参中指定运算符的两个操作数
friend Number operator+(const Number& left, const Number& right){
Number t(left);
t.m_nNum += right.m_nNum;
return t;
}
}
int main()
{
Number obj;
obj = obj + "123";//obj.operator+(Numer("123456"))
obj = 123 + obj;//operator+(Number(123) , obj3)
}
  • 访问控制

    1. public:类内类外都能访问

    2. protected:控制在子类内部能访问,在类外不能访问

    3. private:在子类和类外都不能访问

  • 静态成员

    1. 静态成员变量:静态局部变量就是带作用域的全局变量

      C++ 中的作用域:

      • 复合语句
      • 函数作用域
      • 文件作用域 = 全局作用域
      • 类作用域
      • 命名空间

      静态成员变量是类内定义的,但本质上属于全局变量,作用域是类域

      • 静态成员变量的生存周期和全局变量相同,从程序运行到程序结束(类内成员变量的生存周期是和对象一起,随对象的构造而拥有内存,随对象的析构而失去内存,对象又分为局部对象,全局对象,堆空间对象),因此静态成员变量不属于对象的一部分(即对象的内存中不包含静态成员变量)

      • 静态成员变量的作用域属于类域,当需要在类外使用静态成员变量时,可通过两种方法来访问:

        1. 通过类名:类名::静态成员变量名
        2. 通过对象:对象.静态成员变量名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyClass{
public:
//声明静态成员变量
static int m_nNum;
};
//必须在类外定义静态成员变量
//若无定义,则报错:无法解析的外部符号m_nNum
int MyClass::m_nNum;
int g_nNum;
int main()
{
MyClass::m_nNUm = 10;//使用静态成员变量
//比较两者地址,会发现内存中位置相近
//静态成员变量存储在全局数据区
printf("g_nNum=%p m_nNum=%p\n", &g_nNUm, &MyClass::m_nNum);
}
  1. 静态成员函数

    静态成员函数与一般成员函数的唯一区别就是无 this 指针,因此不能直接访问非静态数据成员,静态函数放在代码区

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class MyClass{
    int m_nNum1;
    static int m_nNum;
    public:
    static void staticFun(){
    //this->m_nNum1 = 0;//静态成员函数无this指针
    MyClass obj;
    obj.m_nNum1 = 0;//通过对象可以访问私有成员
    m_nNum = 10;//可以直接访问静态成员变量
    }
    };
    int MyClass::m_nNum = 0;
    int main()
    {
    MyClass::staticFun();//调用静态成员函数
    }
  • 友元

    1. C++ 提供了三大访问控制权限用于控制类外,类内,子类内成员的访问控制

    2. 友元就是一个类对某个对象授予所有的访问控制权限

      例如某个类的私有成员变量在主函数中不能直接通过对象来访问,但通过友元授权,主函数也能直接通过对象来访问这个类的所有成员

    3. 友元能够授权的对象:

      • 普通友元函数:将类的访问权限全部授予一个普通的函数

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        class MyClass{
        int m_nNum;
        static int m_staticNum;
        //使用friend关键字将某个普通函数的函数头放在类内声明
        //这个普通函数就能成为本类友元函数
        friend int main();
        };
        int MyClass::m_staticNum;
        int main()
        {
        MyClass obj;
        obj.m_nNum = 0;//成为友元之后可以访问私有成员
        MyClass::m_staticNum = 0;
        }
- 友元类:将本类的访问权限全部授予给另一个类(在另一个类的所有成员函数中都能直接访问到授权类的所有成员)

  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyClass{
int m_nNum;
static int m_staticNum;
//声明一个友元类: friend class 类名;
//作用: 该类所有成员函数都能访问本类的私有成员
friend class Class2;
};
int MyClass::m_staticNum;
class Class2{
public:
void fun(MyClass& obj){
//声明友元类之后, 就能在成员函数中
//去访问MyClass类的私有变量
obj.m_nNum = 0;
}
};
- 友元成员函数:将本类的访问权限全部授予给另一个类的某个成员函数(只有被授权的成员函数能够访问所有成员,没有被授权的成员函数访问不了) 互相引用的问题:在A类中使用了B类,B类又使用了A类,类就无法正常声明了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MyClass2 {
public:
void fun1();
void fun2();
};

class MyClass1 {
int m_nNum;
public:
// 声明fun1()为友元成员函数
friend void MyClass2::fun1();
};

void MyClass2::fun1() {
MyClass1 obj;
// 访问私有成员变量
obj.m_nNum = 0;
}
void MyClass2::fun2() {
MyClass1 obj;
// 访问私有成员变量, 但fun2并没有被声明成友元成员函数. 因此访问失败.
obj.m_nNum = 0;
}