C++ 短裤
1. 引用
1.1 定义
- r引用n,r等价与n(别名)
- r就是n,而不是n的副本,对r修改,n也跟着改变
类型名 & 引用名 = 某变量名
- 只能引用变量,不能引用常量和表达式
int n = 4;
int& r = n;
// r 引用了 n, r 的类型为 int&
1.2 应用:引用作为函数返回值
- 可以对函数赋值,??????
1.3 常引用
- 定义引用时,前面加
const
关键字- 单向的,r引用n后,只能通过r访问n,而不能修改n
const int& r = n
// r 的类型为 : const int&
-
不能通过常引用修改,引用内容(即不能通过重新给r赋值,来修改n,会编译报错)
-
const int&
与int&
是不同的类型
- const int& r = int& n 或 const int& r = int n
-
int& r = const n 或 int& r = const int& n(必须强制类型转换)
1.4 const 关键字
- const(=constant): In computer programming, a constant is an identifier with an associated value which cannot be altered by the program during normal execution
- 定义常量、常引用
const int r = 50;
const string r = "O,Fuck"
const int& r = n;
- 常量指针
const int* p = & n
- 不能通过常量指针修改其指向的内容(只读)
- 常量指针的指向可以变化(定义后,可以重新初始化)
- 可以把非常量指针赋值给常量指针,反过来不行(同1.3 -2:总结起来就是:非const可以赋值给const,但const不能赋值给别人,必须强制类型转换。换言之,const不允许被传递,防止传了几手之后,被间接修改)
- 将函数参数写为常量指针,可以防止函数内部修改参数所指的内容
2. 动态内存分配
2.1 new运算符
- new运算符的返回值类型为:指针(指向分配空间起始地址)
- 分配一个变量的空间
int* p;
p = new int;
- 动态分配出一个内存大小为
sizeof(T)
字节的存储空间,并将该内存空间的起始地址赋值给指针p
- 分配一个数组的空间
P = new T[N]
- 分配一个
N*sizeof(T)
字节的存储空间,p指向起始地址
2.2 delete 运算符
- 只能 deletenew出来的空间(p必须指向new出来的空间)
- delete只能做一次,如果对一块new出来的空间delete两次,会导致异常(编译没问题,但可能误删数据)
delete对象 | 用法 | 备注 |
---|---|---|
释放变量空间 | delete p; |
|
释放数组空间 | delete [ ] p; |
p无须指向起始地址,可指数组空间中任意地址 |
3. 内联函数 函数重载 函数缺省参数
3.1 内联函数
- 函数语句少时,调用时间显得大
- 减少函数调用开销
- 跳过函数调用,直接执行函数体(将函数体直接插到调用函数处)
- 定义: inline 关键字
inline int fun(int a, int b){
...
}
3.2 函数重载
- 名字相同,参数表(参数个数,参数类型…)不同的函数之间构成,重载关系
- 目的
重名不用怕,编译器通过参数就知道调用哪个函数
- error:二义性错误
3.3 函数的缺省值
- 缺省值:就是默认值(Default),定义函数时给某些参数提前初始化,如果调用时不写这些参数,就使用缺省值
- 使用
- 缺省参数,统一放到参数表后面
- 调用函数时,不能在中间缺省参数(前后都有传递参数)
void fun(int a, int b = 2, int c = 3){
...
}
fun(10); == fun(10, 2, 3);
fun(10, 8); == fun(10, 8, 3);
fun(10, , 8); != fun(10, 2, 3);
- 目的
- 提高程序的可扩充性,升级更新程序时,如果不传新的参数值,就可以保持原来的默认参数值
类
- 数据结构(抽象事物的属性/数据) + 函数(抽象事物的行为/方法)
- 类的名字就是一种自定义的类型,(class X ,X就是一个自定义的类型,像 int…一样)
- 对象所占空间大小等于所有成员变量大小之和,只包含成员变量,不包含成员函数(不占空间)
使用类的成员变量和成员函数
- 用法 :
①直接:
对象.成员变量(或函数)
②指针:
先定义“类 类型”的指针指向对象, 再用:指针->成员变量
(或函数)
ClassName object;
ClassName * point = & object;
p -> variable = 5;
p -> reference;
③引用:
先定义“类& 类型”的引用,将对象o“重命名”,再通过引用.成员变量(或函数)
调用
ClassName object;
ClassName & reference = object;
reference.variable= 5;
reference.function(....);
类的定义与类的成员函数可以分开学
方法:
class ClassName{
...
};
int ClassName::FunName(){
...
}
- ClassName::FunName() : 类的成员函数
- 不能直接调用,必须通过对象调用的方法(①②③)才能调用
类成员(变量或函数)的可访问范围
- 关键字
private
,public
,protected
:
关键字 | 作用 | 备注 |
---|---|---|
private | 私有成员,只能在成员函数内被访问 | |
public | 公有成员,可在任何地方被访问 | |
protected | 保护成员,***空空空空*** |
- 使用:
class ClassName{
private:
int a;
public:
int b;
void f1(int c, ...);
...
};
- 如果没有这三个关键字,缺省地(= 默认)被认为是私有成员
class ClassName{
int a;
...
};
int main(){
ClassName obj;
printf("%d", obj.a); //error
}
class CEmployee{
private:
char szName[30]; //名字
void f();
public:
int salary; //工资
void setName(char * name);
void getName(char * name);
void averageSalary(CEmployee e1, CEmployee e2);
};
void CEmployee::setName(char * name){
strcpy(szName, name); //①
}
void CEmployee::getName(char * name){
strcpy(name, szName); //①
}
void CEmployee::averageSalary(CEmployee e1, CEmployee e2){
cout << e1.szName; //②
salary = (e1.salary + e2.salary) / 2;
}
void CEmployee::f(){
...
}
int main(){
CEmployee e;
strcpy(e.szName, "Tom1234567890"); //③
e.f(); //③
e.setName("Tom");
e.salary = 5000;
return 0;
}
- ① OK:成员可以访问自己类里的成员
- ② OK: 成员可以访问同类其他对象的私有成员(类似生物中:同物种间,不产生生殖隔离,可以xx)
- ③ ERROR: 一个类的私有成员不可以在类的成员函数外被访问
隐藏:设置私有成员,目的是强制对成员变量的访问一定要通过成员函数进行,
好处:以后成员变量的类型等属性修改以后,只需更改成员函数即可。否则所有直接访问成员变量的语句都要修改。(如果private成员变量的要求变了,可以只将对应public成员函数修改(内部加一些判断语句),来拒绝了不符合“新要求”的private成员变量,这样就可以不用挨个修改不符合新要求的private成员变量了,即通过public成员函数来统一管理privtae成员变量)
(szName[30]时private成员,在main里不能被修改,但可以(只能)通过public成员setName(char * name),来间接修改)
成员函数的重载和缺省
- 同函数的重载、缺省(见3.2-3.3)
- 避免二义性
class ClassName{
void f(int a = 0){
...
}
void f(){
...
}
};
int main(){
ClassName o;
o.f(); //error
}
构造函数(constructor) & 析构函数(destructor)
- 成员函数的一种
- 名字与类名相同
- 可以有参数,但不能有返回值(void 也不行)
- 先生成对象(存储空间),再调用构造函数初始化(先买房再装修)
- 作用
给对象初始化(成员变量赋初值…) - 没有构造函数:
默认生成一个无参数的构造函数(也称:默认构造函数),什么都不干
如果定义了构造函数,就不会生成默认的构造函数 - 只在对象生成时自动调用,对象一旦生成,就再也不能在其上执行构造函数
- 一个类可以有多个构造函数(通过参数来判断调用哪个构造函数)
- 意义:
执行初始化工作,不必专门再写初始化函数,也不用担心忘记调用初始化函数 - 对象没被初始化就使用,会导致程序出错
-
构造函数在对象数组中的使用
pass; -
复制构造函数
只有一个参数,即对同类对象的引用(参数必须是引用)
没有定义复制构造函数,编译器也会生成默认的复制构造函数(默认构造函数不一定存在,复制构造函数一定会存在??)
使用:
ClassName(ClassName & o){
...
}
常引用(一般不会去修改,故一般使用常引用):
ClassName(const ClassName & o){
this_a = o.a;
this_b = o.b;
...
}
class ClassName{
...
};
ClassName o1; //调用缺省无参构造函数
ClassName o2(01); //调用缺省的复制构造函数,将o2初始化和o1一样
复制构造函数起作用的三种情况:
- 一个对象o1初始 化另一个对象o2时:
ClassName o2(o1);
等价于:
ClassName o2 = o1;
//初始化语句,而非赋值语句
- 赋值语句不会调用赋值构造函数!
ClassName o1, o2;
o2 = o1; //赋值语句,不调用赋值构造函数
- 某函数func()有个参数为 “class ClassName” 的对象o1,调用func()时:
void func(ClassName o1){
...
}
int main(){
ClassName o2;
func(o2); //调用 "class ClassName" 的复制构造函数
...
}
- 如果一个函数的返回值是 “class ClassName” 的对象时,函数返回时:
ClassName func(){
ClassName o;
return o;
}
int main(){
cout << func().a <<endl;
...
}
常量引用参数的使用:
- 如果函数参数是 ClassName o ,调用函数是会使用 ClassName 的复制构造函数给 o 初始化,开销较大
- 故采用 引用类型做参数,即ClassName & o (引用不生成副本,就是原变量,换了个名字)
- 若希望确保传入函数的实参在函数中不被改变,加
const
关键字,使传入的参数只读(类似于映像,既不是副本,又不是原变量换名)
Q.为什么要自己写复制构造函数?
!类型转换构造函数:
只有一个参数,而且不是复制构造函数的构造函数,一般就可以看作是转换构造函数
当需要时,编译系统会自动调用转换构造函数,建立一个无名的临时对象(或临时变量)
目的:实现类型的自动转换
class Comples{
public:
double real, imag;
Complex(int i){
//类型转换构造函数
real = i; imag = 0;
}
Complex(double r, double i){
real = r; imag = i;
};
int main(){
Complex o1(7, 8);
Complex o2 = 12; //调用转换构造函数
c1 = 9; //9被自动转换成一个临时的Complex对象
...
}
析构函数(destructors)
- 在对象消亡时起作用(自动调用),对象消亡前作善后工作,比如释放分配的空间…
- 消亡一个对象,调用一次析构函数
- 名字与类名相同,在前面加~
- 没有参数和返回值
- 一个类最多只能有一个析构函数
- 定义时没写,会自动生成缺省析构函数,(什么也不做),定义了析构函数就不再生成析构函数
- 拆房子前,把值钱的东西都搬走
class String{
private:
char * p;
public:
String(){
p = new char[10];
}
~ String();
};
String::String(){
delete [] p;
}
delete运算会导致调用析构函数:
ClassName * p;
p = new ClassName;
delete p; //调用析构函数
p = new ClassName[3]; // 调用构造函数3次
delete [] p; //调用析构函数3次
tips: new一个对象数组时,要用delete [] p,否则只delete一个对象
tips: return出的临时对象使用完(执行完对应语句)也要消亡→调用析构函数
!!构造函数和析构函数什么时候被调用?
- 先构造后析构
!!复制构造函数在不同编译器中的表现:
dev c++中优化:不生成返回值临时对象
this 指针
起因:最初没有C++编译器,要先将C++翻译成C语言
Ⅰ.
class ClassName{
int i;
public:
void Hello(){
cout << "Hello" <<endl;
}
};
int main(){
ClassName * p = NULL;
p->Hello();
}
输出:Hello
Ⅱ.而将成员函数Hello()
改为:
Hello(){
cout << i << "Hello" <<endl;
}
就会出错!
原因:Ⅰ中Hello()函数自带一个ClassName * this参数(类似于python里类的每个成员函数第一个参数必须是self)传进去一个指针可以执行Hello()函数,而Ⅱ中传进p以后,输出的i是p->i,p指向NULL,不存在i,所以错误
- 应用在成员函数中
- 作用就是指向成员函数所作用的对象
class ClassName{
int a;
ClassName func(){
this->a++;
return * this;
};
int main(){
ClassName o1, o2;
o2 = o1.func();
...
}
- 静态成员函数中不能使用this指针
- 因为静态成员函数并不具体作用于某个对象
- 因此,静态成员函数的真实参数的个数就是程序中写出的参数个数,而动态成员函数隐藏了一个 ClassName * this
静态成员
在定义时,前面加一个
static
关键字的成员
1. 普通成员与静态成员区别
- 普通成员变量与静态成员变量区别
区别Ⅰ:
成员 | 作用 | 备注 |
---|---|---|
普通成员变量 | 每个对象有各自的一份 | 每个对象自己的属性 |
静态成员变量 | 所有对象共享,一共就一份 | 所有对象共同的属性 |
区别Ⅱ:
sizeof运算符不会计算静态成员变量
- 普通成员函数与静态成员函数区别
成员|作用|备注
—|---|–
普通成员函数|必须具体作用于某个对象|
静态成员|并不具体作用于某个对象|
2. 访问静态成员
- 通过类:
类名::成员名
ClassName::MemberVariable
ClassName::MemberFunction();
- 通过对象:
对象名.成员名
ClassName obj;
o.memberVariable;
o.MemberFunction();
//只是一种形式,静态成员函数并不作用在对象上
- 通过 “类 类型” 的指针:
ClassName obj;
ClassName * p = & obj;
p->MemberVariable;
p->MemberFunction();
- 通过对象的引用(别名):
ClassName obj;
ClassName & reference = obj;
reference.MemberVariable;
reference.MemberFunction();
3. 总结:
- 静态成员不需要对象就能访问(是一个类的共性)
- 静态成员变量就是全局变量,哪怕一个对象都不存在,类的静态成员变量也存在
静态成员函数本质上就是全局函数,不需要作用在某个对象上 - 设置静态成员这种机制的目的是将和某些类紧密相关的全局变量和函数写到类里,看上去像一个整体,易于维护和理解(若一个类的静态变量写成全局变量,不容易看出全局变量与该类的关系,同时也会误以为全局变量与其他类有关系),静态成员就是一种封装
4. 注意:
- 在定义类的文件中对静态成员变量进行一次说明或初始化(在所有成员函数外面)。否则编译能通过,链接不能通过
- 在静态成员函数中,不能访问非静态成员变量,也不能调用非静态成员函数
- 将静态成员变量的变化写进构造函数和析构函数时,要注意有些是调用复制构造函数(没写就会自动生成),不调用构造函数(如形参中的对象,返回值的对象,临时对象…),而消亡时一定调用析构函数,就会导致的静态成员变量值错误 //解决办法,在类里自己编写对应的复制构造函数
成员对象和封闭类
- 有成员对象(类中有成员是其他类的对象,类间调用)的类叫 封闭(enclosing)类
任何生成封闭类对象的语句,都要让编译器明白,对象中的成员对象,是如何初始化的(即成员对象必须有相应的构造函数)
具体做法:通过封闭类的构造函数的初始化列表
初始化列表中的参数可以是任意复杂的表达式,可以包括函数,变量,只要表达式中的函数或变量有定义就行
!!封闭类构造函数和析构函数的执行顺序
- 封闭类对象生成时,先执行所有对象成员的构造函数,再执行封闭类的构造函数(由里向外构造,先把零件造好,在组装成机器)
- 对象成员的构造函数调用次序和对象成员在类中的说明次序一直,与它们在成员初始化列表中出现的次序无关
- 当封闭类的对象消亡时,先执行封闭类的析构函数,再执行成员对象的析构函数(由外向里,回收机器时,先拆外壳,再拆零件)
实例:
!!封闭类的复制构造函数:
常量对象 常量成员 常引用
- 常量对象
如果不希望某个对象的值被改变,定义该对象时可以在前面加const
关键字(只读) - 常量成员
- 常量成员函数:在类的成员函数说明后面加
const
关键字
class ClassName{
public:
void function() const;
...
};
void ClassName::function() const{
...
}
作用:执行期间,不应修改其作用的对象
\ 不能修改成员变量的只(静态成员变量除外)
\ 不能调用同类的非常量成员函数(静态成员函数除外)
\常量成员函数的重载:
a:
int f() const{
...
}
b:
int f(){
...
}
a和b为重载关系
调用时:
用常量对象const ClassName obj
调用 f(),将会调用 int f() const
//因为常量对象不能执行非常量成员函数
用对象ClassName obj
调用 f(),将会执行 int f()
- 常引用:
见1.3
- 常引用经常作为函数的参数:
对象作为函数的参数,生成该参数要调用复制构造函数(生成临时对象),效率低
指针作为参数,代码不好看
立即推:用对象的引用作为参数
但是:对象引用作参数由一定风险→若函数中不小心修改了形参,实参也会被修改
如果使用常引用,就会避免实参被修改,因为常引用时只读(不能修改原变量)的
友元(friends)
- 友元分为友元函数和友元类
- 朋友,可以访问私有
- 友元函数:
- 可以声明有哪些非本类的成员函数,可以访问本类的私有变量(赋予访问权限)
一个类的友元函数可以访问这个类的私有成员
不是这个类的成员函数,可以是一个普通的全局函数,声明为友元后就可以访问这个类的私有成员了
#include <iostream>
using namespace std;
class CCar;
//提前声明CCar类,以便后面的CDriver类使用
//因为CCar类和CDriver类都互相调用了对方,谁放前面都不好,故提前声明一个
class CDriver{
public:
void ModifyCar(CCar * pCar); //改装汽车
};
class CCar{
private:
int price;
friend int MostExpensiveCar(CCar cars[], int total); //声明友元
friend void CDriver::ModifyCar(CCar * pCar); //声明友元
};
void CDriver::ModifyCar(CCar * pCar){
pCar->price += 1000; //改装后汽车价值增加
}
int MostExpensiveCar(CCar cars[], int total){
//求最贵汽车的价格
int tmpMax = -1;
for(int i = 0; i < total; ++i)
if(cars[i].price > tmpMax)
tmpMax = cars[i].price;
return tmpMax;
}
int main(){
return 0;
}
两种友元函数:
普通的全局函数;
将一个类的成员函数(包括构造、解析函数)说明为另一个类的友元
class A{
public:
void function();
};
class B{
friend void A::function();
};
- 友元类
同友元函数,只是授权的范围变大了,之前授权一个函数可以搞自己的私有成员,现在授权一个类,里面所有成员函数都可以搞自己了
如果在类A里,把另一个类B声明为友元,B的成员函数就可以访问A的私有成员
class A{
private:
int a;
friend class B;
}
class B{
public:
A obj;
int f(){
obj.a++;
...
}
};
//B生成对象的函数f()就可以调用A的私有成员变量a
总结:
增加私有成员的可访问范围
注意:
友元类之间的关系不能传递下去,也不能继承给子类
第四周 运算符重载(Operator overloading)
4.1 基本运算符重载
缺:对象间的运算符
补:基本运算符重载,扩展新的规则
- 把含重载运算符的表达式转换为对运算符函数的调用,把运算符的操作数转换为运算符的参数,可以重载为普通函数,也可以重载为成员函数
- 实质就是函数重载,可以多次重载,根据不同情况,自动调用不同的运算符函数
- 基本形式:
#include <cstdio>;
#include <iostream>;
using namespace std;
class Complex{
public:
double real, imag;
Complex(double r=0.0, double i=0.0) : real(r), imag(i){
//构造函数:将实部与虚部都初始化为0
}
Complex operator -(const Complex & c); //重载为成员函数
};
Complex operator +(const Complex & a, const Complex & b){
//重载为普通函数(全局函数)
return Complex(a.real + b.real, a.imag + b.imag); //返回一个临时对象:用 实部=a.r+b.r,虚部=a.i+b.i初始化
}
Complex Complex::operator -(const Complex & c){
//成员函数,只需要一个参数(参数个数 = 运算符个数-1)
return Complex(real - c.real, imag - c.imag); //返回一个临时对象
// real - c.real :当前对象(this.real)- 参数对象(c.real)
}
int main(){
Complex a(4, 4), b(1, 1), c;
c = a + b; //等价于 c = operator+(a, b);
cout << c.real << "," << c.imag <<endl;
cout << (a - b).real << "," << (a - b).imag <<endl; //a-b 等价于 a.operator-(b);
return 0;
}
- 总结:
1) 重载为全局函数:a + b 等价于 operator +(a, b);
2) 重载为成员函数:a - b 等价于 a.operator -(b);
4.2 赋值运算符重载
缺:= 两边类型不匹配(int a = double b)
补:将 = 重载(定义新规则)
- = 只能重载为成员函数 //因为赋值一定有个载体(对象)承接这个值,所以只能是
obj.operator +(x)
,而不能是operator +(obj, x)
- 实例:将一个字符串赋值给一个String对象
#include <iostream>;
#include <cstring>;
using namespace std;
class String{
private:
char * str; //指向动态分配的存储空间
public:
String() : str(new char[1]){
//构造函数 new一个只有一个元素的char数组,用该数组的指针(数组名)初始化str
str[0] = 0;
}
const char * c_str(){
return str;
};
String & operator =(const char *s); //赋值运算符重载
//Q:为什么返回的是 String &
~String(){
//析构函数
delete [] str;
}
};
String & String::operator =(const char * s){
//重载 = 使obj = “hello”能够成立
delete [] str;
str = new char[strlen(s)+1];
strcpy(str, s);
return * this;
}
int main(){
String s;
s = "Good Luck, "; //等价于s.operator = ("Good Luck, ");
cout << s.c_str() <<endl;
//String s2 = "Hello!"; //这是初始化语句,没写构造函数,所以会出错
return 0;
}
- 分析:
s.operator =("Good Luck, ");
的运行过程:
代码 | 过程 |
---|---|
1.delete [] str; |
先将String对象s的str delete掉(重置) |
2. str = new char[strlen(s)+1]; |
为str重新分配strlen(s)+1(+1放:\0) 大小的空间(重定向) |
3. strcpy(str, s); |
将参数(字符串"Good Luck, ")的内容拷贝过来 |
4.return * this; |
以后讲!!pass |
- Error:
[Error] extra qualification 'String::' on member 'String' [-fpermissive]
→ Reason: The qualification student::
is required only if you define the member functions outside the class, usually in .cpp file.
- 浅拷贝与深拷贝:
- case:
String s1, s2;
s1 = "this";
s2 = "that";
s1 = s2
就出问题了!最后一条s1 = s2
,如果没有重载=,也能执行,相当于将对象s2赋值给对象s1(完全复制):s1.str对自己指的"this"始乱终弃,去指s2.str指的"that"了
后果:1. 2. 3.
3.如果再次给s1赋值,调用operator=( ) 就会 delete掉str指向的空间,就把s2的空间删了
- 改进:
- case:
MyString s;
s = "Hello";
s = s;
→重载函数中增加一个判断:
if(this == &s) //this指向当前对象
return * this;
- 对operator = 返回值类型的讨论:
对运算符进行重载的时候,好的风格是应该保持运算符原本的特性
- case1:
考虑到a = b = c;
:
先进行 b = c
,如果operator =
返回值类型是void,接下来就成了 a = void
就出问题了
a = b = c;
等价于 a.operator =(b.operator(c))
- case2:
(a = b) = c;
a=b的返回值是a的引用,
cpp中赋值运算符的返回值是=左边变量的引用(要维持这个性质),
对a的引用赋值成c,