c++ 重要知识点
1.const修饰指针
-
const修饰指针 --- 常量指针
-
const修饰常量 --- 指针常量
-
const即修饰指针,又修饰常量
/*---常量指针---*/ //const放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。 //常量指针--指的是数值不能通过这个指针来改变。但是本身是可以改变的
int a = 10;
const int * p1 = &a;
a = 20;
*p = 30; //报错
/*---指针常量---*/
//const修饰的是常量,指针指向不可以改,指针指向的值可以更改
//const放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改
int * const p2 = &a;
//p2 = &b; //错误
*p2 = 100; //正确
const 与 # define的区别
(1).类型
const常量是常量的声明,有类型区别,需要在编译阶段进行类型检查;而宏定义是字符替换,没有数据类型的区别,同时这种替换没有类型安全检查,可能产生边际效应等错误。
(2).编译器处理不同
宏定义是一个"编译时"概念,在预处理阶段展开,不能对宏定义进行调试,生命周期结束与编译时期;const常量是一个"运行时"概念,在程序运行使用,类似于一个只读行数据
(3).存储方式不同
宏定义是直接替换,不会分配内存,存储于程序的代码段中;const常量需要进行内存分配,存储于程序的数据段中。
2. void类型
void通常表示无值,但将void作为指针的类型时,它却表示不确定的类型。这种void型指针是一种通用型指针,也就是说任何类型的指针值都可以赋给void类型的指针变量。指针有两个属性:指向变量或对象的地址和长度。
(1).void指针是一种特别的指针
// 说它特别是因为它没有类型
// 或者说这个类型不能判断出指向对象的长度
void *vp
(2).任何指针都可以赋值给void指针
type *p;
vp=p;
//不需转换
//只获得变量/对象地址而不获得大小
(3).void指针赋值给其他类型的指针时都要进行转换
type *p=(type*)vp;
//转换类型也就是获得指向变量/对象大小
(4).void指针不能解引用
*vp //错误
//因为void指针只知道,指向变量/对象的起始地址
// 而不知道指向变量/对象的大小(占几个字节)所以无法正确引用
(5).5.void指针不能参与指针运算,除非进行转换
(type*)vp++;
// vp==vp+sizeof(type)
void * 和 void 在函数返回值中的区别
在函数的返回值中, void 是没有任何返回值, 而 void * 是返回任意类型的值的指针.
#include <stdlib.h>
#include <stdio.h>
void voidc(int a);
void* voidcp(int *a);
int main(){
int a=10;
int *ap;
voidc(a);
ap = voidcp(&a);
printf("%d\n",*ap);
return 0;
}
void voidc(int a){
printf("%d\n",a);
return; // 没有返回值
}
void* voidcp(int *a){
printf("%d\n", *a);
return a; // 返回 int *
}
3. 内存分区模型
五大内存分区分别是:栈区,堆区,BSS区(全局静态区),数据区(常量区),代码区
(1).栈区
-
栈区的内存空间由
系统管理
--> 即方法调用开始时开辟空间,方法调用结束时回收空间。 -
栈区是从
高地址
向低地址
扩展,是一块连续的内存区域
,遵循先进后出,后进先出(FILO)原则
,使用效率高。 -
栈区的内存空间是在
运行时
由系统进行分配。 -
哪些变量是栈区的?例如
方法的入参,内部定义的局部变量
等,都存放在栈区
。
(2).堆区
-
最大的特点 --> 空间大,需程序员自己手动管理。
-
堆区是从
低地址
向高地址
扩展,与栈区
相反,遵循先进先出(FIFO)
的原则。 -
堆区由系统api开辟空间(c/c++ -->
malloc、calloc、realloc
, oc -->alloc
new
),这个空间可以是不连续的
,以链表
结构存在, -
开辟出的空间的
首地址
是在栈区
,例如LGPerson *person = [[LGPerson alloc] init];
这个person指向所指向的地址是在栈区
。 -
内存回收 -->
free回收
,做了两件事
,一是释放堆区的内存,二是将栈区的指针置为nil
(3).BSS区(全局静态区)
-
存放
全局变量
和静态变量
。 -
内存空间也是由
系统管理
-->程序启动
时开辟
,程序结束
时回收
,程序执行期间一直存在
。 -
static
修饰的变量仅执行一次
,生命周期为整个程序运行期
。
(4).data区(数据区/数据段)
-
存放常量(整型、字符型,浮点型,字符串等),整个程序运行期
不能被改变
。-
已初始化
的全局变量
-
-
已初始化
的静态变量
-
空间由
系统管理
,生命周期为整个程序运行期
。
(5).代码区(代码段)
-
存放cpu执行的机器指令,代码区是可共享,并且是只读的。
内存泄漏(memory leak):是指申请的内存空间使用完毕之后未回收。一次内存泄露危害可以忽略,但若一直泄漏,无论有多少内存,迟早都会被占用光,最终导致程序crash
。(因此,开发中我们要尽量避免内存泄漏的出现)
内存溢出(out of memory):是指程序在申请内存时,没有足够的内存空间供其使用。
说明:
BSS段、数据段、代码段是在编译时候分配,堆栈和堆积是运行时候分配。
C++中利用==new==操作符在堆区开辟数据,堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符==delete==
4.引用
int a = 10;
int& ref = a; //等价于 ==》 int* const ref = &a;
//返回局部变量引用
int& test01() {
int a = 10; //局部变量
return a;
}
//返回静态变量引用
int& test02() {
static int a = 20; //静态的局部变量存放在全局区,程序结束以后才会被释放。
return a;
}
int main() {
//不能返回局部变量的引用
//在windows环境下的编译器可以这样做
//但在liunx环境下不可以,原因如下
/*
* 函数的返回值为函数内部定义变量的引用,但函数在调用完毕后,函数内部定义的变量空间被释放,无法访问,从而造成的错误。
* 当 int a = 10; 时,是局部变量。函数调用结束,局部变量会被销毁。无法返回变量的引用地址。
* 所以需要加上static修饰
*/
int& ref = test01();
//cout << "ref = " << ref << endl; //第一次结果正确,是因为编译器做了保留
//cout << "ref = " << ref << endl; //第二次就是错误的,因为a的内存已经被释放了
//如果函数做左值,那么必须返回引用
int& ref2 = test02();
cout << "ref2 = " << ref2 << endl;
cout << "ref2 = " << ref2 << endl;
test02() = 1000; //相当与tase02()和变量a操作的是同一个内存
cout << "ref2 = " << ref2 << endl;
cout << "ref2 = " << ref2 << endl;
system("pause");
return 0;
}
左值引用和右值引用:(c++11的新特性)
有一个很好区分左值和右值的方式,就是是否可以对表达式取地址。可以获取地址的表达式就是左值,且持久性变量都是左值,反之则是右值。
int i = 42; //i是左值,可以对i取地址
int &r = i; //r是左值引用,绑定左值i
int &&rr2 = i * 42; //不能对表达值取地址,所以表达式结果是右值,可以绑定到右值引用
int &&rr1 = 42; //不能对字面常量取地址,所以字面常量是右值,可以绑定到右值引用
左值通常是变量,数组元素,结构体成员,应用。
右值通常是常量,常量表达式。
const int &r2 = i * 42; //不能对表达值取地址,所以表达式结果是右值,可以绑定到const左值引用
5.c++创建对象的两种方式
-
不通过“new”关键字,在栈上创建。在栈中的对象,其作用范围只是在函数内部,函数执行完成后就会调用析构函数,删除该对象。使用 “.” 而不是 “->” 调用对象的方法。
-
通过关键字“new”,在堆上创建。在堆中的对象,必须要程序员手动的去管理该对象的内存空间 。用运算符“->”调用对象的方法。new出来的对象必须用指针指明地址。
A a; // a存在栈上
A* a = new a(); // a存在堆中
1 前者在栈中分配内存,后者在堆中分配内存
2 动态内存分配会使对象的可控性增强
3 大程序用new,小程序不加new,直接申请
4 new必须delete删除,不用new系统会自动回收内存
6.拷贝构造函数
拷贝构造函
数是一种特殊的构造函数,其形参是本类对象的引用。拷贝构造函数的作用是在建立一个新对象时,使用一个已存在的对象去初始化这个新对象。每个类都必须有一个拷贝构造函数。可以自己定义拷贝构造函数,用于按照需要初始化新对象;如果没有定义类的拷贝构造函数,系统就会自动生成一个默认拷贝构造函数,用于复制出与数据成员值完全相同的新对象。
class Score{
public:
Score(int m, int f); //构造函数
Score();
Score(const Score &p); //声明拷贝构造函数
~Score(); //析构函数
void setScore(int m, int f);
void showScore();
private:
int mid_exam;
int fin_exam;
};
Score::Score(int m, int f)
{
mid_exam = m;
fin_exam = f;
}
//定义拷贝构造函数
Score::Score(const Score &p)
{
mid_exam = p.mid_exam;
fin_exam = p.fin_exam;
}
Score sc1(98, 87);
Score sc2(sc1); //调用拷贝构造函数
Score sc3 = sc2; //调用拷贝构造函数
调用拷贝构造函数的三种情况:
-
当用类的一个对象去初始化该类的另一个对象时;
-
当函数的形参是类的对象,调用函数进行形参和实参结合时;
-
当函数的返回值是对象,函数执行完成返回调用者时
浅拷贝和深拷贝
浅拷贝
就是由默认的拷贝构造函数所实现的数据成员逐一赋值。通常默认的拷贝构造函数是能够胜任此工作的,但若类中含有指针类型
的数据,则这种按数据成员逐一赋值的方法会产生错误。
class Student{
public:
Student(char *name1, float score1);
~Student();
private:
char *name;
float score;
};
//如下语句会产生错误
Student stu1("白", 89);
Student stu2 = stu1;
上述错误是因为stu1和stu2所指的内存空间相同,在析构函数释放stu1所指的内存后,再释放stu2所指的内存会发生错误,因为此内存空间已被释放。解决方法就是重定义拷贝构造函数,为其变量重新生成内存空间。
Student::Student(const Student& stu){
//申请新的内存空间
name = new char[strlen(stu.name) + 1];
if (name != 0) {
strcpy(name, stu.name);
score = stu.score;
}
}
构造函数调用规则如下:
-
如果用户定义有参构造函数,c++不在提供默认无参构造,依然会提供默认拷贝构造
-
如果用户定义拷贝构造函数,c++不会再提供其他构造函数(如果需要其他的构造函数就需要自己提供)系统提供的拷贝构造函数会将所有的属性值都拷贝一份。
7.函数重载
(给函数赋予另一种功能,以适应不同的数据类型)。
同一个函数名对应不同的函数实现,每一类实现对应着一个函数体,名字相同,功能相同,只是参数的类型或参数的个数不同。多个同名函数只是函数类型(函数返回值类型)不同时,它们不是重载函数。
int add(int a,int b)
{
return a+b;
}
double add(double a,double b)
{
return a+b;
}
int add(int a,int b,int c)
{
return a+b+c;
}
//返回值的类型不同,不是函数的重载
double add(int a,int b,int c)
{
return a+b+c;
}
7.内联函数
在函数名前冠以关键字inline
,该函数就被声明为内联函数。每当程序中出现对该函数的调用时,C++编译器使用函数体中的代码插入到调用该函数的语句之处,同时使用实参代替形参,以便在程序运行时不再进行函数调用。引入内联函数主要是为了消除调用函数时的系统开销,以提高运行速度。
说明:
-
内联函数在第一次被调用之前必须进行完整的定义,否则编译器将无法知道应该插入什么代码
-
在内联函数体内一般不能含有复杂的控制语句,如for语句和switch语句等
-
使用内联函数是一种空间换时间的措施,若内联函数较长,较复杂且调用较为频繁时不建议使用
#include <iostream>
using namespace std;
inline double circle(double r) //内联函数
{
double PI = 3.14;
return PI * r * r;
}
int main()
{
for (int i = 1; i <= 3; i++)
cout << "r = " << i << " area = " << circle(i) << endl;
return 0;
}
8.C++ 类的三种访问权限
(1).公共权限 public 成员:类内可以访问,类外可以访问任何一种继承,子类可以访问父类的公共成员 (2).保护权限 protected 成员:类内可以访问,类外不可以访问任何一种继承,子类可以访问父类的保护成员 (3).私有权限 private 成员:类内可以访问,类外不可以访问。任何一种继承,子类不可以访问父类的私有成员