C++的内存分区
代码区、全局区、栈区、堆区
代码区
存放二进制,程序未执行前,分成两个区域:代码区、全局区
共享的:复用
只读的:防止意外更改
全局区
存放全局变量、静态变量、常量、const修饰的全局变量(全局常量)
这个区域的数据在程序结束后,由操作系统释放
栈区
程序运行后,此区域数据由操作系统管理
不要返回局部变量的地址,因为其由编译器自动释放
int* func() {
int a = 10;
return &a;
}
int main() {
int* p = func();
cout << *p << endl;//第一次编译器会保留这个数据
cout << *p << endl;//第二次就会出错
return 0;
}
堆区
程序员管理生存周期,程序结束系统自动回收
new就是在堆区开辟内存
new返回的是堆区变量的地址
new int[10]表示需要在堆区创建一个长度10的数组
int* func() {
//用new在堆区开辟内存
int *p=new int(10);
return p;//这里的指针p实际上也是一个局部变量,放在栈区。但指针指向的数据int(10)是放在堆区的
}
int main() {
int* p = func();
cout << *p << endl;
cout << *p << endl;
return 0;
}
释放堆区的变量是delete,释放数组的时候要用delete[]
C++引用
作用:给变量起别名
int main() {
int a = 10;
cout << "a: " << a << endl;
int& b = a;
b = 20;
cout << "b: " << b << endl;
cout << "a: " << a << endl;
return 0;
}
引用的注意事项
1、引用必须初始化
int &b;//这个写法是错误的
2、引用初始化后就不能变了
int a=10;
int c=100;
int &b=a;
int &b=c;//不允许
引用做函数参数
值传递(使用引用可以让形参啊修饰实参,简化指针)
地址传递
引用传递
//值传递
void swap1(int a,int b) {
int tmp;
tmp = a;
a = b;
b = tmp;
cout << "swap1 a: " << a << " swap1 b: " << b << endl;
return;
}
//地址传递
void swap2(int *a,int *b) {
int tmp;
tmp = *a;
*a = *b;
*b = tmp;
cout << "swap2 a: " << *a << " swap2 b: " << *b << endl;
return;
}
//引用传递
void swap3(int &a,int &b) {
int tmp;
tmp = a;
a = b;
b = tmp;
cout << "swap3 a: " << a << " swap3 b: " << b << endl;
return;
}
int main() {
int a = 1;
int b = 100;
swap1(a, b);
swap2(&a, &b);
swap3(a, b);
return 0;
}
引用做函数返回值
注意:不要返回局部变量引用
函数的调用可以作为左值
int & test01() {
int a = 10;
return a;//不能返回局部变量的引用
}
int& test02() {
static int a = 10;
return a;//不能返回局部变量的引用
}
int main() {
//int& ref = test01();
//cout << "ref: " << ref << endl;//ref: -858993460
int& ref = test02();
cout << "ref: " << ref << endl;//ref: 10
//函数调用可以作为左值
test02() = 1000;
cout << "ref: " << ref << endl;//ref: 1000
return 0;
}
引用的本质
本质:引用的本质在C++内部的实现是一个指针常量(int * const ref=&a)
//当作为函数参数时,内部发现是引用,转换为int * const ref=&a
void func(int& ref) {
ref = 100;
}
int main() {
int a = 10;
//自动转换为int * const ref = &a;指针常量是指针指向不可变,指向的值可以变,这就说明了为什么别名初始化之后就不能更改了
int& ref = a;
ref = 20;//内部发现ref是引用,自动转换为*ref=20
cout << "a: " << a << endl;
cout << "ref: " << ref << endl;
func(a);
return 0;
}
常量引用
作用:常量引用主要用于修饰形参,防止误操作,防止形参改变实参
void showValue(const int& v) {
//v += 10;//只要用const限定了引用,这里对引用的修改就会报错
cout << v << endl;
}
int main() {
int a = 10;
int& ref = a;
showValue(ref);
return 0;
}
C++函数高级
默认参数
int func(int a, int b = 10, int c = 20) {
return a + b + c;
}
int main() {
int result=func(30);
cout << result << endl;
return 0;
}
注意事项
1、如果一个位置已经有了默认参数,那么这个位置之后必须设置默认参数
2、如果函数声明有默认参数,那么它的实现不能包含默认参数
函数占位参数
注意:调用函数时必须填充这个位置!
int func(int, int b = 10, int c = 20) {
return b + c;
}
int main() {
int result=func(100);
cout << result << endl;
return 0;
}
目前阶段,这个占位的int参数还用不上
函数重载
满足条件
1、同一作用域下
2、函数名称相同
3、函数参数类型不同、个数不同或顺序不同
注意
返回值不同不能作为重载的条件
void print(int a) {
cout << a << endl;
return;
}
void print(int a,int b) {
cout << a <<" " << b << endl;
return;
}
int main() {
print(1);
print(1, 2);
return 0;
}
函数重载的注意事项
引用作为重载条件
void func(int &a) {
cout << "func(&a)调用" << endl;
}
void func(const int& a) {
cout << "const int& a调用" << endl;
}
int main() {
int a = 10;
func(a);//func(&a)调用
func(100);//const int& a调用
//因为int &a=100不合法,引用的对象需要是一个堆区或者栈区的数据,而100是个常量,放在全局区所以不合法;
//而const int &a=100,常量=常量,语法正确所以只能调用这个
return 0;
}
函数重载碰上默认参数
void func2(int a,int b=20) {
cout << "func2(int a,int b=20)调用" << endl;
}
void func2(int a) {
cout <<"func2(int a)调用" << endl;
}
int main() {
//func2(10);//此时这种调用,两个重载的函数都能进去,出现二义性,会报错
return 0;
}
类和对象!
三大特性:封装、继承、多态
C++中,万事万物都是对象
封装
封装的意义:把属性和行为放在不同的权限下加以控制
class Stu {
private:
int sid;
string sname;
int sage;
public:
void set_sid(int sid) {
this->sid = sid;
return;
}
void set_name(string sname) {
this->sname = sname;
return;
}
void set_sage(int sage) {
this->sage = sage;
return;
}
int get_sid() {
return this->sid;
}
string get_sname() {
return this->sname;
}
int get_sage() {
return this->sage;
}
void printInfo() {
cout << "sid: " << get_sid() << " " << "sname: " << get_sname() << " " << "sage: " << get_sage() << endl;
}
};
int main() {
Stu newstu;
newstu.set_sid(1);
newstu.set_name("alex");
newstu.set_sage(21);
newstu.printInfo();
system("pause");
return 0;
}
public 类内外都可以访问
private 只有类内可以访问
protect 类外不可以访问,儿子可以访问父类
struct和class的区别
struct默认访问权限为public
class默认访问权限为private
将成员属性设置为私有private
优点
1、可以自己控制读写权限
2、对于写权限,可以检测数据的有效性
构造函数和析构函数
初始化和清理是一个安全问题,编译器自动提供并调用
构造函数:类名(){}
1、没有返回值也不写void
2、函数名和类名相同
3、可以有参数,因此可以重载
4、自动调用且仅调用一次
析构函数:~类名(){}
1、没有返回值也不写void
2、函数名和类名相同,加~
3、不可以带参数
4、自动调用且仅调用一次
class Stu {
public:
Stu() {
cout << "构造函数被调用 "<< endl;
}
Stu(string s) {
cout << "构造函数被调用 s: " << s << endl;
}
~Stu() {
cout << "析构函数被调用 " << endl;
}
};
int main() {
Stu newstu;
Stu newstu2("alex");//带参数的构造函数是这样初始化的!
system("pause");
return 0;
}
构造函数的分类及调用
两种分类
按参数:有参构造和无参构造
Stu() {
cout << "构造函数被调用 "<< endl;
}
Stu(string s) {
cout << "构造函数被调用 s: " << s << endl;
}
按类型:普通构造和拷贝构造
class Stu {
private:
int sid;
string sname;
public:
Stu() {
cout << "构造函数被调用 "<< endl;
}
Stu(string s) {
cout << "构造函数被调用 s: " << s << endl;
}
//拷贝构造函数
Stu(const Stu(&stu)) {//const是为了防止误操作修改了本体的值,拷贝构造包含了本体所有的属性和方法
sname = stu.sname;
}
void set_sname(string sname) {
this->sname = sname;
}
void get_sname() {
cout<<this->sname << endl;
}
};
int main() {
Stu s1;//默认构造函数的调用
s1.set_sname("alex");
Stu s2(s1);//拷贝构造函数的调用
s2.get_sname();
system("pause");
return 0;
}
三种调用方式
括号法
Stu s1;//默认构造函数的调用
s1.set_sname("alex");
Stu s2(s1);//拷贝构造函数的调用
使用默认构造函数时,不要写成
Stu s1();//编译器会把这个当成一个函数的声明
显示法
Stu s1;
Stu s2 = Stu(s1);//多了一步显式的赋值
Stu;//使用无参的构造函数生成的一个匿名对象,这一行结束就会析构
s1.set_sname("alex");
s2.get_sname();
注意
不要用拷贝构造函数来初始化一个匿名对象
Stu s1;
Stu(s1);//这个匿名对象无法被创建,因为编译器自动转换为Stu s1;与上面重定义
隐式转换法
Stu s2="alex"//等价于Stu s2=Stu("alex")
Stu s3=s2//拷贝构造,等价于Stu s3=Stu(s2)
拷贝构造函数的调用时机
1、使用一个已经创建完毕的对象来初始化一个新的对象
Person p1(21);
Person p2(p1);
2、值传递的方式给函数参数传值
void dosomething(Person p) {//此时会调用拷贝构造函数来生成这个形参
}
void test1() {
Person p;
dosomething(p);
}
int main() {
test1();
system("pause");
return 0;
}
3、以值方式返回局部对象
Person dosomething2() {
Person p;
return p;//因为是以值的方式返回的,所以这里返回的时候会创建一个新对象,构造函数用的就是拷贝构造函数
}
void test2() {
Person p = dosomething2();
}
int main() {
test2();
system("pause");
return 0;
}
构造函数的调用规则
默认情况
默认情况下C++编译器至少给一个类添加了三个函数:
默认构造函数、默认析构函数、默认拷贝构造函数
调用规则
用户提供有参构造函数,编译器就不提供默认构造函数了,但仍然会提供默认拷贝构造函数
用户提供了拷贝构造函数,编译器就不会提供默认拷贝构造函数
深拷贝和浅拷贝
面试题常出
浅拷贝
简单的赋值拷贝操作
浅拷贝时,堆区的空间会重复释放
class Person{
public:
Person() {
cout << "无参构造函数调用" << endl;
}
Person(int age,int height) {
this->mage = age;
this->mHeight = new int(height);//开辟在堆区,堆区的数据需要在析构时释放
cout << "有参构造函数调用" << endl;
}
//Person(const Person & p) {
// cout << "拷贝构造函数调用" << endl;
//}
~Person() {
cout << "析构函数调用" << endl;
if (mHeight != NULL) {
delete mHeight;
mHeight = NULL;//为了防止野指针出现
cout << "mHeight已释放" << endl;
}
}
void set_age(int age) {
this->mage = age;
}
int get_age() {
cout << "age: " << this->mage << endl;
cout << "height: " << *this->mHeight << endl;
return this->mage;
}
private:
int mage;
int* mHeight;//为了把数据开辟在堆区
};
void test01() {
Person p1(18,180);
//如果使用编译器提供的拷贝构造函数,会执行浅拷贝,逐字节拷贝
//因为在栈内,先进后出,p2先被析构,要释放此时堆区的mHeight
//当p1被析构时,也会去释放堆区的mHeight,导致堆区重复释放的异常
Person p2(p1);
p1.get_age();
p2.get_age();
}
int main() {
test01();
system("pause");
return 0;
}
深拷贝
在堆区重新申请空间,进行拷贝
堆区重复释放的解决方法,自定义深拷贝构造函数
//自定义深拷贝构造函数
Person(const Person & p) {
cout << "深拷贝构造函数调用" << endl;
mage = p.mage;
mHeight = new int(*p.mHeight);//在执行拷贝构造时,单独在堆区开辟一片空间
}
初始化列表
作用:c++提供用来初始化属性
特别注意这个冒号的位置,在小括号后面!
//初始化列表赋初值
Person(int id,string name,int age,int height):mid(id),mname(name),mage(age),mHeight(height) {
}
要注意的是,这个构造函数也是重载的
类对象作为类成员
一个类的对象可以是另一个类
class Phone {
public:
Phone() {
cout << "Phone无参构造函数调用" << endl;
}
~Phone() {
cout << "Phone析构函数调用" << endl;
}
};
class Person{
public:
Person() {
cout << "Person无参构造函数调用" << endl;
}
~Person() {
cout << "Person析构函数调用" << endl;
}
string pname;
Phone pphone;
};
void test01() {
Person p1;
}
int main() {
test01();
system("pause");
return 0;
}
输出:
Phone无参构造函数调用
Person无参构造函数调用
Person析构函数调用
Phone析构函数调用
构造时先构造对象,析构时相反
静态成员
静态成员变量
所有对象共享
编译阶段分配内存
类内声明,类外初始化
静态成员函数
所有对象共享
静态成员函数只能访问静态变量
class Person{
public:
Person() {
cout << "Person无参构造函数调用" << endl;
}
~Person() {
cout << "Person析构函数调用" << endl;
}
static void print() {
//pname = "peter";
cout << "hello " << endl;
cout << secret << endl;
}
static string secret;
string pname;
};
//静态成员变量很特殊,类内声明,类外初始化
string Person::secret = "Hello world!";
void test01() {
Person p1;
//两种访问方式
//1、通过对象
p1.print();
//2、通过类名
Person::print();
}
静态成员函数不能访问非静态成员变量,因为静态成员在内存中只有一个副本,而非静态成员在每一个对象中都有一个副本,所以这个静态方法无法区分要修改哪一个非静态变量
静态成员也可以指定private等访问权限
C++对象模型和this指针
成员变量和成员函数分开储存
在C++中,类内的成员变量和成员函数分开储存
只有非静态成员变量才属于类的对象
class Person{
float a;
static int b;
void func() {
}
static void func2() {
}
};
void test01() {
Person p1;
cout << sizeof(p1) << endl;
}
空对象占用1字节,因为C++编译器会给每个空对象也分配1字节
当添加了一个int类型的属性时,新对象占4字节
再添加一个静态 int b,新对象仍占4字节
再添加一个void方法,仍然占4字节
this指针
this指针指向被调用的成员函数所属的对象
this不用定义
作用:
1、解决名称冲突(如果构造函数使用的形参和成员变量重名,编译器认不出来)
class Person {
Person(int age) {
age = age;
}
int age;
};
2、返回对象本身用*this
以上例为例
class Person{
public:
float a;
static int b;
void func() {
cout << sizeof(this) << endl;
}
static void func2() {
}
};
void test01() {
Person p1;
cout << sizeof(p1) << endl;
p1.func();
}
this指针占8字节,解指针后,*this(就是p1)占4字节(也就是当前成员函数被调用的p1的大小)
class Person{
public:
Person(int age) {
this->age = age;
}
//因为返回的是一个特定的Person对象,所以要以引用的方式来接返回值
Person& PersonAddAge(Person &p) {
this->age += p.age;
return *this;//这里*this解指针返回的是一个Person对象
}
int age;
};
void test01() {
Person p1(10);
Person p2(10);
p2.PersonAddAge(p1).PersonAddAge(p1).PersonAddAge(p1);
cout << p2.age << endl;
}
Person PersonAddAge(Person &p) {
当这里返回值类型是Person时,表示返回一个值类型的数据,就会调用Person的拷贝构造函数,创建一个新的对象,就不是原来的p2了
空指针访问成员函数
C++允许空指针访问成员函数,但是要注意函数体中有没有用到this指针
class Person{
public:
Person(int age) {
m_age = age;
}
void showClassName() {
cout << "this is person class" << endl;
}
void showPersonAge() {
cout << "person age: " << m_age << endl;
}
int m_age;
};
void test01() {
//Person p1(10);
//p1.showClassName();
//p1.showPersonAge();
Person* p = NULL;//初始化空指针
p->showClassName();//调用这个函数没有问题
p->showPersonAge();//调用这个函数会崩溃,因为函数体中使用了m_age,默认会加上this->m_age
}
const修饰成员函数
常函数
-
成员函数后加const后称为常函数
-
常函数内部不可以修改成员属性
-
成员属性声明时加了mutable后,在常函数中可以修改
常对象
- 声明对象前加const称该对象为常对象
- 常对象只能调用常函数
- 成员属性声明时加了mutable后,在常对象中可以修改
class Person{
public:
Person(int id,int age) {
m_age = age;
m_id = id;
}
//this指针的本质是 指针常量 指针的指向是不可以修改的
//this = NULL; //这里会报错,this指针不可以修改指向,但是this指针指向的值可以修改
void func() const //加上这个const,就是const Person* const this,表示this的指向和指向的值都不能修改
{
//m_age = 100;//加上const,常函数就不能修改成员变量了
m_id = 1;//加上mutable的成员变量是可以在常函数中修改的
}
void sayHello() {
cout << "hello" << endl;
}
int m_age;
mutable int m_id;
};
void test01() {
//非常对象,常函数和非常函数都能调用
Person p1(1,20);
p1.func();
p1.sayHello();
//常对象,只能调用常函数
const Person p2(2,21);
p2.func();
//p2.sayHello();//语法会报错
}