1 new和delete
- new的返回值是对应数据类型的地址,数组时返回首地址
- delete之后再让指针指向NULL是一个很好的习惯
int *p = new int(10);
delete p;
int *arr = new int[10];//开一个10个元素的数组,访问数组时与正常数组一样使用,例如元素arr[3]
delete[] arr;//释放数组时加一个中括号
2 引用
- 给变量起别名,引用和原来的变量操作的是同一块内存地址。数据类型 &别名 = 原名
- 引用做函数参数,即形参作为了实参的别名,可用形参修饰实参,其比地址传递效率更高
1.按值传递
2.按地址传递(形参可修饰实参)
3.按引用传递(形参可修饰实参)
void swap(int &a, int &b)
{
int temp = a;
a = b;
b = temp;
}
简单的swap函数即可验证
引用效率高的三个原因:
1.无需解引用:使用引用作为函数参数时,不需要进行指针解引用(*)操作。
2.无需空指针检查:引用作为函数参数时,不会出现空指针的情况。因为引用必须在声明时进行初始化,而且不能被重新赋值为nullptr或空指针。而指针作为函数参数时,需要进行空指针检查,以确保指针有效,否则可能导致错误。
3.代码可读性:引用作为函数参数可以提高代码的可读性和易用性。它可以让函数调用看起来更自然,更直观,并且减少了在使用指针时可能出现的繁琐的语法。
- 常量引用:主要用来限制形参,防止函数中误操作影响实参,如const int &
int &a = 10;//不允许,引用要绑定的是左值,要对合法空间进行操作
const int &a = 10;//允许
注意事项:
- 引用必须初始化。如:int &b;//错误
- 引用一旦初始化不可更改。它已经作为一个变量的别名时,就不允许再做其它变量的别名了。(本质上就是一个指针常量)
- 不要返回局部变量的引用,编译器不会报错这类问题,但是在使用这个返回值时会出问题。局部变量在函数结束后就应该被释放掉了,使用这样的引用相当于操作非法空间了。
int& swap(void)//不要这样做,如果要返回a,必须加上static来延长生命周期
{
int a = 20;
return a;
}
3 函数默认参数与占位参数
默认参数:允许在函数声明中为一个或多个参数指定默认值。当调用函数时,如果没有为这些参数提供实参,则会使用默认值作为参数的值。
void printMessage(const std::string& message, int count = 1) {
for (int i = 0; i < count; ++i) {
std::cout << message << std::endl;
}
}
int main() {
printMessage("Hello"); // 使用默认参数 count = 1
printMessage("Hi", 3); // 提供实参,count = 3
return 0;
}
占位参数:通常用于表示需要一个参数,但不关心其值、类型等
4 函数重载
允许函数名相同,提高复用性
函数重载条件;
- 同意作用域下
- 函数名称相同
- 函数参数类型不同,或者顺序不同,或者个数不同
注:返回值不可作为重载条件,尽量避免占位参数的函数重载
5 封装
封装是一个很典型的区别于C的特性,它允许数据(属性)和函数(行为)放在一起,可提供接口,限制属性
封装的权限控制:
- 私有:类内的成员可以访问,类外不可以(继承时子类不可访问基类私有)
- 保护:类内的成员可以访问,类外不可以(继承时子类可以访问基类的保护)
- 公共:类内类外都可以访问
C++中struct和class的区别只有默认访问权限不同
6 对象特性
1 构造与析构
分别用于对象的初始化和清理,自己不实现的时候,编译器会帮助实现
构造函数:
- 语法:类名(){}
- 允许有参数,可重载
- 创建对象时,自动调用一次
析构函数:
- 语法:~类名(){}
- 不允许有参数,不可重载
- 对象销毁前调用,一般用于清理在堆区开辟的内存
2 构造函数的调用以及调用
按参数分:有参构造和无参构造
按类型分:普通构造与拷贝构造
拷贝构造函数:
用于从其它已有的相同类型对象身上拷贝出一份一模一样的数据
Person (const Person &p){}
三种调用方式
1.括号法
主要用于传参
Person p1;//默认构造函数,不需要加括号
Person p1(10);//有参构造
Person p2(p1);//拷贝构造函数
2.显示法(几乎不用)
几乎不用,过于繁琐
Person p2 = Person(10);//等式右边相当于一个匿名对象
Person p3 = Person(p3);//拷贝构造
3.隐式法(几乎不用)
Person p2 = 10;
Person p3 = p2;
3 拷贝构造函数使用时机
- 使用一个已创建的对象来初始化一个新对象
- 值传递的方式给函数参数传值
- 以值方式返回局部对象
值传递方式调用:
void doWork(Person p){
}
void test(){
Person p;
dowork(p);//在传参的过程中调用了拷贝构造函数
}
以值方式返回局部对象
在调用这个函数进行赋值时会调用拷贝构造函数
Person doWork2()
{
Person p1;
return p1;
//注意这里返回的并不是真的p1,而是p1',是一个新对象,若要返回p1,函数类型应该是Person&
}
4 构造函数调用规则
- 每创建一个类,编译器都会为我们自动添加三个函数,默认构造,析构函数,拷贝构造。
- 如果我们只写了有参构造,编译器将不再提供默认无参构造,但任然提供拷贝构造
- 如果我们写了拷贝构造,编译器将不再提供其它构造函数
默认->有参构造->拷贝构造,后面任意一个实现了,前面的都不会再由编译器提供
这就是为什么我们看到好多程序中会同时写上多个构造函数。
5 深拷贝与浅拷贝
浅拷贝:像编译器提供的这样的默认拷贝构造中,就只是简单的赋值拷贝操作。对于在堆区开辟地址的变量,这种浅拷贝使得两个对象的成员指向了同一块堆区地址,最后在调用析构释放的时候会造成堆区内存重复释放,即内存的非法操作。
深拷贝:在堆区重新申请空间,进行拷贝构造,解决浅拷贝出现的问题。解决方式就是自己实现拷贝构造函数,对于要拷贝的变量重新用new申请一块内存来存放。
如果属性有在堆区开辟的,一定要自己提供一个深拷贝构造函数
6 初始化列表
语法:构造函数():属性1(值1),属性2(值2)...{}
方便我们使用构造函数进行值得初始化
Person(int a, int b, int c):m_A(a), m_B(b), m_C(c){}
7 静态成员
在成员变量前加上static关键字就成为静态成员,这是专属于类的,且非静态成员函数也是属于类的,只有非静态成员变量才是属于对象的,可以用sizeof来了解
静态成员变量
- 不属于某一个对象,所有对象共享同一份数据,即有一个对象对这个成员变量进行了修改,另外的对象拥有的也是这个修改过的数,所有对象的这个静态成员变量的值应当是一样的
- 在编译阶段分配内存
- 类内声明,类外初始化,如:Persson::m_A = 10;
- 两种访问方式:通过对象访问,通过类访问
静态成员函数
- 所有对象共享一个函数
- 静态成员函数只能访问静态成员变量,因为静态成员变量在内存中只有一份,而其它非静态成员变量无法区分是哪一个对象的。
- 两种访问方式:通过对象、通过类名
8 this指针
C++中对于成员函数只有一份实例,无论是静态还是非静态的,多个对象调用这个函数的时候会共享这一块代码,C++用this指针来区分是哪一个对象在调用。
this指针隐含在每一个非静态成员函数内,指向调用该成员函数的对象,其用途为:
- 当形参和成员变量同名时,用this指针来区分,this指示的就是要被赋值的成员变量
- 在类的非静态成员函数中返回对象本身,可以使用return *this
Person& PersonAddAge(Person &p)
{
this->age += p.age;
return *this;//返回这个对象
9 空指针访问成员函数
为了避免我们使用空指针访问,好的办法应该是这样的
if(this == NULL) return;
10 const修饰成员函数
常函数:
- 成员函数加const后变为常函数
- 常函数内不可以修改成员属性,除非加了关键字mutable
void addAge() const{}//常函数
常对象:
- 声明对象前加const,不允许修改成员
- 常对象只能调用常函数,因为普通函数能修改成员变量