目录
一、C语言基础
在学习C++之前,打好C语言基础至关重要。
首先介绍C语言与C++的区别:
① C语言是C++的基础,C++是C语言的扩充。
② C语言是面向过程的语言,C++是面向对象的语言。
③ 因为C++是面向对象的语言,所以C++增加了类,继承,多态,泛型,STL库。
本章从以下几个方面学习C语言:
1. 函数&变量&运算符
1.1 函数参数传递
💗 1.1.1 函数参数传递原理
在函数参数传递过程中,由于函数参数是临时变量(按传值),因此,其内存空间是栈空间,函数参数是以栈的形式进行存取的,根据函数参数的顺序,从右至左入栈。如:void fun(int x,char y,double z)
,在调用函数时,实参从double z
到int x
依次入栈。
💗 1.1.2 传值与传址
在C++语言中函数有两种传参的方式: 传值和传址。
以传值方式: 在函数调用过程中会生成临时变量用形参代替,并把实参的值传递给新分配的临时变量即形参。 但在函数调用结束后,实参不会跟随形参而改变。注意:局部变量在函数结束时会被释放销毁。
如果要改变实参的值, 只能通过指针传递。如果函数是“只读函数”,为了防止数据/地址传递增加破坏数据的风险,需要在形参中增加const
标识符来解决。
💗 1.1.3 可变参数(不明确参数)
在日常的编程中,常见的函数参数是通过自定义的方式进行明确的。但当我们无法列出传递函数的所有实参的类型和数目时,可以用省略号指定参数表。如char msg(const char *format,...)
。
#include<stdio.h>
#include <stdarg.h>
using namespace std;
void msg(const char *format,...){
char buffer[256];
va_list strlist; //一个字符指针,为指向当前参数的一个指针(format),对可变参数取参必须通过这个指针进行
va_start(strlist,format); //对va_list进行初始化,让va_list指向可变参数表里面的第一个参数(format后面的第一个参数)
//用于向字符串中打印数据、数据格式用户自定义。
//通过字符指针strlist,读取format后边的参数,并按format格式合并为完整字符串。
vsnprintf(buffer,256,format,strlist);
printf("%s\n",buffer); //输出为Hello test1 test2 test3
va_end(strlist); //删除字符指针
}
int main(){
char *a="test1";
char *b="test2";
char *c="test3";
msg("Hello %s %s %s",a,b,c);
}
1.2 函数的递归
递归常用于重复性的程序当中。在递归中,每次递归调用都会创建自己的一套变量,保存在不同的地址空间当中。如下所示,可以用栈的入栈和出栈来表示递归。注意:当程序有很大的递归时,会导致地址空间消耗完而导致程序异常退出。
1.3 inline 内联函数
Q1. 为什么要使用inline,能不能将所有函数设为inline?
在C语言中,如果一些函数被频繁调用,不断地有函数入栈,即函数栈,会造成栈空间或栈内存的大量消耗。inline
函数的使用可以省去函数调用的开销来提高执行效率, 但是如果inline
函数体内代码执行时间相比函数调用开销较大,则使用inline
函数的意义就不大了。同时,由于inline函数在调用时需要对函数体进行复制,所以会消耗大量空间。因此,当函数过长时,不适用inline
函数(会造成代码膨胀),且inline
函数不能在for
循环中使用。
Notice: inline
函数在编译时取决于编译器的“主观意向”,若内联函数中存在复杂逻辑控制时,编译器不会在认为它是一个内联函数。因此,不是内联函数中不能有循环语句,而是当内联函数中出现了复杂的逻辑控制语句后,编译器会不再认为它是一个内联函数。因此,inline
函数如果存在循环时,编译器会将inline
函数看作是普通函数。
Q2. inline函数怎么用 ?
在函数定义之前添加inline
关键字。当编译器处理调用内联函数的语句时,不会将该语句编译成函数调用的指令,而是直接将整个函数体的代码插人调用语句处,就像整个函数体在调用处被重写了一遍一样。
Q3. 内联函数inline与宏定义define的区别 ?
① 宏定义是预编译器上符号表的简单文本替换,会在预编译时展开,不能进行参数有效性检测及使用C++类的成员访问控制。
② 内联函数是通过参数的传递实现的,会在编译时展开。
#define SQUARE(X) X*X //宏定义
#define SQUARE1(X) ((X)*(X)) //宏定义
inline double square(double x){ //内联函数
return x*x;
}
int main(){
cout<<square(5.0+3.0)<<endl; // (5.0+3.0)*(5.0*3.0) 输出结果为64
cout<<SQUARE(5.0+3.0)<<endl; // 5.0+3.0*5.0+3.0 输出结果为23
cout<<SQUARE1(5.0+3.0)<<endl; // ((5.0+3.0)*(5.0+3.0)) 输出结果为64
}
1.4 函数默认参数
默认参数是指当函数调用中省略了实参时自动使用的一个值。函数参数默认值设置必须通过函数原型进行设置,且带参数函数必须从右向左添加默认值。
char *left(const char *str,int n=1); // √ 函数原型,带参数函数必须从右向左添加默认值
char *left(const char *str,int m=1,int j); // × 从右向左添加默认值,不能跳过任何参数
char *left(const char *str,int m=1,int j=2); // √
left("theory",3); //函数调用,3将覆盖默认值
left("theory"); //函数调用,使用原型默认参数
1.5 函数重载
函数重载是指可以有多个同名的函数,根据上下文来确定要使用的重载函数的版本。函数重载是根据函数的参数列表(函数特征标)而不是根据函数类型。两个函数的参数数目和类型相同,同时参数的排序也相同时,他们的特征标也相同。其中类型引用和类型本身视为同一个特征标。
void staff(double & rs);
void staff(const double & rcs); //函数重载,函数的参数不同
long gronk(int n,float m) ;
double gronk(int n, float m); //不是函数重载,只有函数的返回值不同,而参数相同则不是函数重载
当函数基本上执行相同的任务,但使用不同形式的数据时,才应采用函数重载。如果两个重载函数,只是参数的个数不同,类型相同,则可以使用一个带默认参数的函数要简单一些。
char *left(const char *str,int n); 使用默认参数
char *left(const char *str); ========> char *left(const char *str,int n=1);
Q1. 重载函数使用的注意事项 ?
① 函数的实参与形参结合是从左到右顺序进行的,因此指定默认值的参数必须放在形参的最右端。如果程序中既有函数的声明又有函数的定义时,则定义函数时不允许再定义参数的默认值。
② 如果函数的定义在函数调用之前,则应在函数定义中给出默认值。如果函数的定义在函数调用之后,则在函数调用之前需要有函数声明,此时必须在函数声明中给出默认值,在函数定义时可以不给出默认值。
③ 一个函数不能既作为重载函数,又作为有默认参数的函数,否则会出现二义性。
void fun(int); //重载函数之一
void fun(int,int = 2); //重载函数之二,带有默认参数
void fun(int = 1,int = 2); //重载函数之三,带有默认参数
void fun1(float a,int b=0,int c,char d=’a’); //不正确
void fun2(float a,int c,int b=0, char d=’a’); //正确,指定默认值的参数必须放在形参的最右端
fun(3); //error,出现二义性,系统无法判断调用哪一个重载函数
💗 1.5.1 函数重载的本质
函数的重载是C++ 静态多态的实现方式之一,函数重载主要是通过编译器来实现的。在函数重载过程中主要面对的两个问题:
Q1. 编译器如何解决函数的命名冲突 ?
C++编译器是通过函数符号的修饰来解决函数命名冲突的问题的。
● 在C语言的编译器中规定,C语言源代码文件中的所有全局变量和函数经过编译后,相应的符号名是在变量名或函数名前加上 “_”,也因为如此,C语言中很容易产生符号冲突,也不支持函数重载。
● 在C++中,为了能够解决C语言中符号冲突,对函数和变量名称进行了修饰,形成符号名。C++符号修饰的规则如下:
Q2. 编译器如何选择调用对应匹配的函数 ?
编译器实现调用重载函数解析时,首先找到同名的候选函数,然后从候选函数中找到最合适的,找不到则报错。重载函数的解析过程如下:
在选择对应匹配的重载函数中,最重要的是重载函数的解析过程,重载函数的解析分为三步:
① 根据函数名称确定候选函数集:采用深度优先搜索的方法,从函数运行的起始点开始查找,逐层作用域向外查找可见的候选函数。
② 从候选函数集中选择可用函数集合:根据上图中的重载函数解析规则选择合适的可用函数。
③ 从可用函数集中确定最佳函数,若出现模棱两可的情况则返回编译错误。
1.6 函数模板
函数模板允许以泛型的方式编写程序。函数模板不能缩短可执行程序,最终的代码不包含任何模板。编译器使用模板为特定类型生成函数定义得到的是模板的实例,模板并非函数的定义,使用模板实例是函数定义。模板的实例分为显式实例化和隐式实例化。当模板显式实例化时的参数类型不同时,可强制类型转换。
模板—>模板实例---->函数定义
template <typename T> //template和typename 为关键字,T为任意名称
void Swap(T &a, T &b);
void Swap(T *a, T *b,int n); //函数模板原型
template <typename T> //函数模板定义 template和typename 为关键字,T为任意名称
void Swap(T &a, T &b){
T temp;
temp=a;
a=b;
b=temp;
}
void Swap(T *a, T *b,int n){ //函数模板的重载
T temp;
for(int i=0;i<n;i++){
temp=a[i];
a[i]=b[i];
b[i]=temp;
}
}
int main(){
int aa=13,bb=19; //函数模板可以代入不同类型的数据
double daa=1.3,dbb=1.9;
Swap(aa,bb); // aa=19,bb=13; //隐式实例化
Swap<double>(daa,dbb); // 显式实例化,编译器直接创建特定的实例
}
1.7 表达式声明与typedef
💗 1.7.1 表达式声明
在C和C++中,声明表达式只有一条规则:按照使用的方式来声明。C变量的声明都是由两部分组成的:类型,以及一组类似表达式的声明符。声明符类似于表达式,对它求值应该返回一个声明中给定类型的结果。
typedef
为C语言的关键字,作用是为一种数据类型定义一个新名字。使用
typedef
目的一般有两个,
一个是给变量一个易记且意义明确的新名字,另一个是简化一些比较复杂的类型声明。
typedef
的具体作用如下:
//1.定义一种类型的别名:
typedef char* PCHAR;
PCHAR pa, pb; //相当于 char* pa,pb
//2. 定义结构体对象名
typedef struct test{
int x;
}t;
//3. 平台无关性:当跨平台时,只要改下typedef本身就行,不用对其他源码做任何修改。
typedef double REAL;
typedef float REAL;
typedef long double REAL;
//4. 掩饰复合类型
typedef char Line[20]; //此时Line类型即代表了具有20个元素的字符数组
Line text,str; //相当于char text[20],str[20]
scanf("%s",test);
typedef char * pstr;
int mystrcmp(const pstr, const pstr); //notice: const pstr被解释为char* const,而不是const char*
//5. 简化代码声明
void (*b[10]) (void (*)()); //原声明
typedef void (*pFunParam)();
typedef void (*pFunx)(pFunParam);
pFunx b[10]; //简化后声明
1.8 sizeof() 运算符
sizeof()
是C/C++的一个运算符,sizeof()
不是函数,sizeof()
是C/C++中一个宏定义,通过指针步长来实现空间大小的计算。
//非数组的sizeof()
#define _sizeof(T) ((size_t)(T*)0+1);
//数组的sizeof()
#define array_sizeof(T) ((size_t)(&T+1)-(size_t)(&T))
sizeof()
的基本语法如下:
sizeof(object); //sizeof(对象)
sizeof(type_name); //sizeof(类型)
sizeof object; //sizeof 对象
int i;
sizeof(i); // √
sizeof(int); // √
sizeof i; // √
sizeo int; // ×
sizeof()
的常见用法如下:
2. 指针
C语言中,变量存放在内存中,每一个字节都有唯一的内存地址,指针是保存变量地址的变量。
2.1 左值与右值
可以取地址的,有名字的是左值。左值表示的是可以存储结果值的内存地址,表达式结束后依然存在的持久化对象。不能取地址,没有名字的就是右值。右值表示的是结果值,表达式结束时就不再存在的临时对象。
Q1. ++i与i++的区别及实现方式 ?
2.2 指针的使用
声明一个指针变量不会自动分配任何内存,在对指针进行访问之前,指针必须进行初始化,否则就是一个野指针。例如,以下程序运行时会报错Segmentation fault (core dumped)
#include "stdio.h"
int main(){
int *p; //指针初始化时未定义,指针指向不明内存
*p = 1; //此时会将不明内存的内容修改,这是很危险的操作,可能导致系统的崩溃。
printf("%d\n",*p);
int x=1;
int *d=&x; //这是正确的
return 0;
}
2.3 指针与数组
数组是用于储存多个相同类型数据的连续集合。
指针是一个地址变量,可以指向数组的首地址。
int a[10];
int *p=a; //用指针p来指向a[10]数组的首地址
2.4 野指针
所谓野指针是指向的位置不可知的指针变量。野指针很危险,应该避免出现野指针。
野指针的产生原因如下:
① 指针变量未初始化。
② 指针用free()和delete释放后未置空。
③ 指针操作超越变量作用域。
3. 左值引用
根据左值与右值,引用也分为左值引用和右值引用,常用的引用一般是左值引用,左值引用是一个永久对象的别名,在C语言中只包含左值引用,右值引用是C++11的新特性。这里只介绍左值引用。
Q1. 为什么要使用引用 ?
引用是一个变量的别名。
① 引用的目的主要用于在函数参数传递中,解决大块数据或对象的传递效率和空间不如意的问题。
② 用引用传递函数的参数,能保证参数传递中不产生副本,提高传递的效率,且通过const的使用,保证了引用传递的安全性。
③ 注意,不要将函数中的局部变量作为引用返回,函数执行完毕后,局部变量会被销毁,导致引用指向不存在的数据。
double dval=3.14; 过程 step1: const int temp=dval; //创建临时常量
const int &ri=dval; =====> step2: const int &ri=temp; //将临时常量绑定到常量引用
Q2. 引用的本质与原理 ?
C++编译器在编译过程中用指针常量作为引用的内部实现,因此引用所占用的空间大小与被引用的指针相同。从使用的角度,引用只是一个别名,C++隐藏了引用的存储空间。
Q3. 引用怎么用 ?
① 通过引用传值(引用作为参数)。
② 函数返回引用(引用作为返回值)
int &fun(int &a,int &b){ //引用作为参数和返回值
a=a+b;
return a; //此时返回值为a+b,变量a为a+b
}
Q4. 引用 与 指针 有哪些不同之处 ?
① 指针是一个指向变量地址的变量,因此指针可以为空;引用为变量的别名,引用必须在定义的时候绑定到某个对象,引用不能为空。
② 指针可以改变指向的对象;引用不能改变绑定的对象。
③ 对指针进行sizeof得到的是指针本身占用的内存大小,32位系统是4字节,64位系统是8字节;对引用进行sizeof得到的是被绑定的变量占用的内存大小。
④ 指针可以有二级,三级等多级指针,引用没有
4. C语言内存管理
内存是程序运行的主要存储场所,程序中的变量,函数,临时变量都存储在内存当中。程序中的任何一个变量,函数等都需要在内存中进行内存空间的分配。在这里内存分配是分配的是虚拟内存,只有当程序运行时,操作系统页面调度系统会发生缺页中断,从而将虚拟内存映射到物理内存中,然后才正真正运行。
4.1 C语言内存分配方式
C语言中,内存分为5个区:栈区(stack),堆区(heap),全局/静态存储区(static),常量存储区(const),代码区。
int a=0; //全局初始化区
char *p; //全局非初始化区
void fun(){
int b; //栈
char s[]="Hello"; //栈
char *p1; //栈
char *pStr="Hello"; //Hello\0在常量区,pStr在栈上
static int c=0; //全局初始化区
p=(char *) malloc(10); //堆,malloc申请内存成功返回void *,因此需要类型强制转换
p1=new char(10); //堆
}
💗 4.1.1 内存栈
Q1.内存栈有什么特点 ?
① 内存栈通常存储局部变量和函数调用后返回地址。在函数执行结束时,这些存储空间被自动释放。
② 栈内存的分配效率高,由操作系统和编译器自动分配,不存在内存碎片问题。
③ 内存栈的存储空间有限,其空间分配是连续的地址空间,未被初始化的静态变量被设置成0。
Q2. 为什么栈的分配效率要比堆高 ?
栈由操作系统控制,在编译时分配空间,在程序启动时,系统就分好了。而堆是运行时动态分配空间,用完了要还给系统,在申请和释放的过程开销就比较大。
💗 4.1.2 内存堆
Q1.内存堆有什么特点 ?
① 内存堆由new/malloc
进行空间的申请和管理,由用户控制,在程序结束时需要delete/free
来释放。
② 内存堆的存储空间比内存栈大。
③ 由于内存堆是动态分配,在内存申请和释放过程中存在开销,因此其效率低于内存栈。
Q2. malloc 与 new有什么区别 ?
① 动态分配内存位置:malloc
在堆上动态分配内存,new
从自由存储区(C++抽象概念,其实际还是在堆中)分配内存。
② 内存分配失败时的返回值:malloc
分配失败时会返回NULL。new
操作符内存分配失败时会抛出bac_alloc
异常。
③ 内存分配成功时的返回值:malloc
分配成功时返回的是void *
,因此malloc
通常会进行类型的转换。new操作符分配成功时返回对象类型的指针。
④ 是否需要指定内存大小:malloc
需要显式的指出所需内存的大小。new
操作符不需要指定内存的大小,编译器会自行计算。
⑤ 是否调用构造/析构函数:malloc
不需要调用构造/析构函数。new
操作符需要调用构造/析构函数。
⑥ 是否可以被重载:malloc
不可以被重载,new
操作符可以被重载。
⑦ 当内存不足时:malloc
能够调用realloc
进行内存重新分配。new
没有直观的方法来扩充内存。
⑧ new
可以调用malloc
,malloc
不能调用new
。
💗 4.1.3 全局区 global/static
static最重要的作用是:变量/函数隐藏,保持内容的全局性。其本质是保证变量或函数的在作用域内的唯一性。
Q1.什么时候使用static ?
当需要一个数据对象为整个类而非某个对象服务,同时又不破坏类的封装性,即要求此成员隐藏在类的内部,对外不可见。
① static局部变量:
static局部变量存储在全局区,当退出函数时,不会被释放。
void test() {
static int a = 10;
cout << a++ << endl;
cout << a << endl;
}
int main() {
test(); //a: 10 11
test(); //a: 11 12
}
② static全局变量
普通的全局变量对整个工程可见,其他文件可以通过extern外部声明后使用该全局变量,因此,该全局变量在工程中是唯一的。
static全局变量,使该变量仅对该文件可见,其他文件不可访问。同时其他文件中的同名变量与该文件互不影响。
//源文件1.cpp
static int a=0; //定义静态变量a=0;
//源文件2.cpp
extern int a; //×,这里编译无错,但链接出错,因为源文件1中的a是对外部不可见的
③ static成员变量
– static成员变量必须在类外初始化, 因为static成员变量只属于整个类,不属于某个对象,如果在类内初始化,会导致每个对象都包含该static成员变量。
– static成员变量存储在全局数据区,在定义时分配存储空间,所以不能在类声明中定义。
– static成员变量是类的成员,无论定义多少个类的对象,static成员变量只有一个,且任一对象都可以对static成员变量进行操作。因此,static成员变量不属于任何一个对象。而非static成员变量,每个对象都会对成员变量进行拷贝。
– static成员变量初始化格式为:<数据类型><类名>::<静态数据成员名>=<值>
– static成员变量访问方式:<类对象名>.<静态数据成员名> 或 <类类型名>::<静态数据成员名>
class Test {
public:
static int b;
private:
static int a; //private static,类对象不可访问
};
int Test::a = 15; //static成员变量初始化,类外初始化
int Test::b = 10; //static成员变量初始化,类外初始化
int main() {
Test *t = new Test();
cout << t->b << " "<<Test::b; //调用static成员变量的两种方式
}
④ static成员函数
● static成员函数可以在类内或类外定义,但必须在类内声明;
● static成员函数属于整个类,而不是某一个对象,因此可以通过类名访问类的公有static成员函数。
● static成员函数没有this指针,它无法访问属于类对象的非static成员变量和非static成员函数,只能调用其他static成员函数。
● 非static成员函数可以任意访问static成员函数与成员变量。
● static成员函数不能被声明为const,也不能声明为虚函数。
class Test {
public:
static void stafun() {
cout << "static function" << endl;
}
};
int main() {
Test *t = new Test();
t->stafun();
while (1);
}
💗 4.1.4 常量区 const
1. const 的底层实现
const
意为常量,通常的理解,const
修饰的变量是不可修改的。但是const
只能在编译期间保证常量被使用时的不变性,无法保证运行期间的行为。因此,如果直接修改const
会得到一个编译错误,但是使用间接指针修改内存,只要符合语法则不会得到任何错误和警告。
在一般情况下,编译器是不为const
创建空间的,只是将这个定义的数字保存在符号表中的。只有在以下两种情况时编译器会为const
定义的常量分配空间:
① 当const
常量是全局变量时,且需要在其他文件中通过extern
使用时。
② 当使用取地址符(&)
取const
常量的地址或引用时。
2. const 应用
① const 变量 / 引用
const int a / int const a
const int &a / int const &a
● const
变量在创建时必须初始化,且创建后,其值无法更改。
● const
变量可以赋值给non-const
变量,反之不行;
● const
变量不能赋值给non-const
引用,因为引用是别名,non-const
引用会修改原const
的值。
● const
变量仅在单个文件中有效,多个文件的const
变量时独立的。
② const 函数
const int getData() { }
由于函数会把返回值进行复制,所以在普通函数前加const
修饰没有意义。
const int *getData() { }
该函数的返回值指针指向的内容是常量。
③ const 成员函数 – 只读函数
int getData(int a,int b) const { }
● const
成员函数只能读取成员变量不能修改成员变量
● const
成员函数不可以改变非multable
成员变量的值
注意:当类中既有const
成员函数,也有non-const
成员函数,且两者函数内部完全相同时,通常令non-const
函数使用const_cast<T&>
转型,调用const
函数。这样既可以避免const
成员函数与non-const
成员函数重复,也可以使non-const
也调用const成员函数。const_cast
用于去除const
限定,static_cast
用于强制转换,可以将non-const
转换为const
。
④ const 对象
A const a / const A a;
● const
对象只能调用 const
成员函数,non-const
对象既可以调用non-const
成员函数,可以调用const
成员函数;
● const
对象可以访问普通成员变量,但不能修改成员变量
⑤ const 指针
const int *p
p指向int型常量
int * const p
p是一个指向int的const指针
const在指针左边,指针所指的内容是常量,const在指针右边,指针本身是常量
#include<iostream>
using namespace std;
class Test_static{
public:
Test_static(std::string s):text(s){}
const char &operator[](std::size_t pos) const{
std::cout<<"const"<<std::endl;
return text[pos];
}
//令non-const函数使用const_cast<T&>转型,调用const函数
char &operator[](std::size_t pos){
std::cout<<"non-const"<<std::endl;
return const_cast<char&> (static_cast<const Test&>(*this)[pos]); //这里non-const函数调用了const函数
//this指针-> const Test& -> char &
}
private:
std::string text;
};
class Test{
public:
//3.const成员函数只能访问成员变量,不能修改成员变量
int getSum(int a,int b) const {
// dataa=a; //会报错 error:assignment of member ‘Test::dataa’ in read-only object
// datab=b; //会报错error:assignment of member ‘Test::datab’ in read-only object
return dataa+datab;
}
void setData(int a) { //非const成员函数
data=a;
}
private:
int data;
int dataa;
int datab;
};
const int sumA(int a, int b) { //2.const函数 此做法无意义
int S = a + b;
return S;
}
int main() {
//1.const变量/引用
const int a = 10; //创建时必须初始化
const int &pa = a;
//int &pb=a; //编译会报错,const对象不能转为non-const对象
//2.const函数
int a = sumA(10, 15); //此做法无意义
//3.const成员函数
Test * t=new Test();
t->getSum(10,12);
const std::string str("Hello");
Test_static t(str);
std::cout<<t[4]<<std::endl;
//首先会调用non-const成员函数,然后调用const成员函数
//输出为non-const -> const -> o
//4. const对象
const Test t; //const对象只能调用const成员函数
cout<<t.getData()<<endl;
//5. const指针
int data=10;
int other_data=20;
const int *a=&data; //a是一个int型指针,a所指的内容是const
// *a=20; //会报错 error: assignment of read-only location ‘* a’
int *const b=&data; //b也是一个int指针,指针b本身不能指向其他地址
// b=&other_data; //会报错 assignment of read-only variable ‘b’
while (1);
}
4.2 数组与指针的内存分配
在大多数的程序中,经常出现字符数组char str[]
和字符串指针char *str
,虽然它们最终的显示的效果是一样的,但是它们却有着本质的区别。
以字符串为例,通常一个字符串可以表示为char str1[5]="Hello";
或者char *str2=“Hello”;
,并且存在str2=str1
。
① 在 char str1[5]="Hello";
中,数组中的单个字符都是可以被修改的,但是str1
表示的是指向这个数组的地址常量,是不能被修改的,str1
始终指向这个字符数组。这个数组的存储空间是在栈中,其中的每一个元素是通过复制将字符存入该地址中的。所以当定义 char str1[5]="Hello";
时,编译器会给数组分配5个单元,每个单元的数据类型为char
,同时其中的每一个元素都是可以修改的。
② 在 char *str2=“Hello”;
中,指针str2
位于栈,而字符串“Hello”
位于常量区,指针str2
只是指向了这个位置。所以对于字符串指针,是不能改变单个元素的值,如str2[0]='P'
是错误的,且在定义时编译器只会分配一个4字节的空间(32位)用于保存地址,因此当char *作为函数参数时,通过sizeof
只能获得char *
指针的大小,而不能获取字符串的长度。
void Test(char *str){
int size=sizeof(str);
cout<<size<<endl; //size=8/4(64位/32位)
}
int main(){
char *t="hlo";
Test(t);
}
③ 但如果是通过new来定义字符串,char *pStr = new char[StrLen];
是可以修改内容的,因为通过new关键字,字符串分配为堆内存中。
char *str="Hello"; //hello 存放在常量区,str存放在栈中
char s[10]="World"; //存放在栈中,字符串数组以\0为结尾
cout<<strlen(s)<<sizeof(s)<<endl; // strlen=5,sizeof=10
char *p=s; //字符串指针指向了字符串数组的首地址
str[1]='a'; //发生段错误,hello在常量区,不能更改
p[1]='a'; //正确,World在栈中,可以修改
④ 对于数组越界问题,如果一个数组越界,其结果可能显示正常,但是越过了数组的实际空间,访问到了不可预知的地址,越界后的数组虽然分配了内存,但用户无法对越界的数组进行控制,造成内存的泄漏。
4.3 C/C++内存对齐
Q1.为什么要进行内存对齐,什么是内存对齐 ?
现代计算机的内存空间是按字节(byte)为单位的,但大部分的CPU并不是按单字节来读取内存的,CPU一般会以2字节,4字节,8字节等存取粒度来存取内存。因此这样要求存在内存的数据的首地址的值必须是内存粒度的整倍数,(如以4字节存取粒度CPU处理int类型数据,该CPU只能从地址为4的倍数的内存开始读取数据)。这就是内存对齐。
Q2.内存对齐规则?
① 数据成员对齐:struct
或union
的数据成员中,第一个数据成员放在offset为0的地方,以后的每个数据成员的存储起始位置是该成员大小或成员的子成员(数组,struct等)大小的整数倍。
② struct
作为成员:如果struct
作为成员,则要从其内部最大的基本类型成员的整数倍地址开始存储。
③ 内存结尾:struct
的总大小必须是内部最大基本类型成员的整数倍。不足的要补齐。
④ union
作为成员:在union
中,所有的共用体成员共用一个空间,并且同一时间只能储存其中一个成员变量的值。所以要以内部最大的基本类型成员的大小作为union的大小。
💗 4.3.1 C++ 内存对齐的设置
在GCC编译器中,支持两种内存对齐设置方式:
① __attribute__((packed))
:取消内存对齐,或者说是1字节对齐
② __attribute__((aligned(n)))
:设定结构体类型整体按n字节对齐,注意是整体而不是这个结构体变量内的元素按n字节对齐,只能是加在类型后面,不能加在变量后面。
struct mystruct1
{
int a;
char b;
short c;
}__attribute__((packed)); //此时结构体类型按1字节对齐,所以这个结构体类型占7字节
struct mystruct2
{
int a;
char b;
short c;
}__attribute__((aligned(2))) mystr2; //此时结构体类型就按2字节对齐
5. 字符串
5.1 字符与ASCII
5.2 字符串的存储方式
字符串是非常重要的数据类型。字符串是由若干个字符组成的序列。在C/C++中的每个字符串都以字符'/0'
作为结尾,因此,每个字符串中都有一个额外字符的开销。如果没有留出额外字符的空间,就会造成字符串的越界。
为了节省内存,C/C++把常量字符串放在内存常量区。当通过指针赋值时,实际上是指向相同的内存地址,但如果是通过常量内存初始化数组时,不同的数组的内存地址是不同的。
//在下边的例子中,字符串“Hello World”是存放在常量区中的
//利用该字符串对数组进行赋值时,每个数组拥有自己的空间地址,因此str1≠str2,且sizeof(str1)=sizeof(str2)=12
char str1[]="Hello World";
char str2[]="Hello World";
//利用该字符串对指针进行赋值时,每个指针共同指向常量区地址,因此str1=str2,且sizeof(str1)=sizeof(str2)=8(64位)/4(32位)
char *str3="Hello World";
char *str4="Hello World";