基础知识复习
宏
无参宏
有参宏:有参宏中使用自增运算符时,导致逻辑上的错误,且无法避免
1
2
3
4
5
6
7
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
5
6
7
8
9inline 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
是常量修饰符:左定值,右定向常量必须被初始化
使用常量时,编译器会直接将常量名用它的初始值来替换(和无参宏一样,在原码中使用宏名时,经预处理后会被替换成宏的内容)
使用场合
修饰指针
定义指针时,
*
在const
左边,指针是一个指针常量;*
在const
右边,指针是一个常量指针常量指针:将指针指向的内容修饰为常量
1
2
3
4
5
6
7
8
9
10
11
12
13char 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
3
4
5
6
7
8
9
10void 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共用内存
}
类型转换
const_cast
:常量类型转换,去除常量类型1
2
3
4
5
6
7
8
9
10
11
12
13void 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);
}
static_cast
:静态类型转换,编译器认可1
2
3
4
5
6
7
8void fun(int n){}
int main()
{
//隐式转换,编译器将double类型的3.14转为int型再传参
fun(3.14);//语法能通过,但会报一个警告:精度丢失...
fun((int)3.14);//C
fun(static_cast<int>(3.14));
}
reinterpret_cast
:强制类型转换,编译器不认可,用于编译器无法进行隐式转换时的类型转换1
2
3
4
5
6int main()
{
int* p = 0x403000;//语法报错,类型不匹配
p = (int*)0x403000;//C
p = reinterpret_cast<int*>(0x403000);
}
dynamic_cast
:动态类型转换,用于将父类和子类的指针或引用进行转换(继承时会用到,能将基类转换为派生类)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class 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);
}
面向对象-类
成员变量
一个对象的数据组成,对象的内存空间就是由成员变量组成;成员在类中的声明顺序决定了成员变量在内存中的顺序
成员变量的使用
需要提供一个对象才能使用成员变量
在成员函数内,
this
指针来表示当前对象1
2
3
4
5
6
7
8class 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;
}
成员函数
成员函数内自带一个
this
指针,通过对象调用函数时 c++ 自动将对象内存首地址赋给this
指针,这样,成员函数被调用后this
指针就保存着调用了这个成员函数的对象变量普通成员函数
需要对象才能调用
通过
this
指针也能调用其他情况和普通函数一样,可以设置默认参数,也可以进行函数重载
构造函数
1.构造函数是一个特殊的成员函数,作用是构造一个对象,无返回值,因为返回值默认就是一个类对象
2.构造函数自动调用
定义对象:
1
2
3
4
5MyClass 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 )
}
构造函数的一些术语:
默认构造:指的是没有形参的构造函数,由编译器默认提供,在某些场合编译器需要自动调用一个类对象的构造函数时,只能调用默认构造,例如:子类继承了父类,当子类对象被构造的时,父类的构造也会被自动调用,此时就只能自动调用父类的默认构造
转换构造:指的是那些只有一个形参,且参数类型是非本类类型的构造函数们,一般能够显式调用(例如
MyClass obj(5)
),也能隐式调用:fun(5)
拷贝构造:指的是只有一个形参,且参数类型是本类类型的引用,一般是在定义一个对象时,将另一个对象作为初始值,就会自动调用这个版本的构造函数,一般编译器会默认提供一个拷贝构造,默认提供的拷贝构造会将对象的内存空间进行拷贝
类中包含有指针成员的时候,一般就需要自己编写拷贝构造,实现对指针指向的内存进行拷贝的功能,而默认拷贝构造是不会去拷贝指针指向的内容(深拷贝和浅拷贝的区别)
带参构造:含有两个以上的形参的构造函数统称带参构造
析构函数
作用和构造函数相反,当一个对象被销毁的时候,就会调用析构函数
运算符重载函数
除三目运算符、
sizeof
、.
、::
运算符外,其他运算符都能重载在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)
}
访问控制
public:类内类外都能访问
protected:控制在子类内部能访问,在类外不能访问
private:在子类和类外都不能访问
静态成员
静态成员变量:静态局部变量就是带作用域的全局变量
C++ 中的作用域:
- 复合语句
- 函数作用域
- 文件作用域 = 全局作用域
- 类作用域
- 命名空间
静态成员变量是类内定义的,但本质上属于全局变量,作用域是类域
静态成员变量的生存周期和全局变量相同,从程序运行到程序结束(类内成员变量的生存周期是和对象一起,随对象的构造而拥有内存,随对象的析构而失去内存,对象又分为局部对象,全局对象,堆空间对象),因此静态成员变量不属于对象的一部分(即对象的内存中不包含静态成员变量)
静态成员变量的作用域属于类域,当需要在类外使用静态成员变量时,可通过两种方法来访问:
- 通过类名:
类名::静态成员变量名
- 通过对象:
对象.静态成员变量名
- 通过类名:
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);
}
静态成员函数
静态成员函数与一般成员函数的唯一区别就是无
this
指针,因此不能直接访问非静态数据成员,静态函数放在代码区1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class 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();//调用静态成员函数
}
友元
C++ 提供了三大访问控制权限用于控制类外,类内,子类内成员的访问控制
友元就是一个类对某个对象授予所有的访问控制权限
例如某个类的私有成员变量在主函数中不能直接通过对象来访问,但通过友元授权,主函数也能直接通过对象来访问这个类的所有成员
友元能够授权的对象:
普通友元函数:将类的访问权限全部授予一个普通的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14class 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;
}