int main()
{
int a = 10;
int b(10);
int c{ 10 };
int d = int(10);
int e = int{ 10 };
//指针初始化
int* f = NULL;
int* g{ NULL };
}
int main()
{
//数组初始化
int ar[10] = { 1,2,3 };
int br[10]{ 1,2,3 };
int cr[] = { 1,2,3 };
int dr[]{ 1,2,3 };
}
调用函数的_s函数,因为有安全保护。
对象 | 含义 | 对应设备 | 对应的类 | c语言中相应的标准文件 |
---|---|---|---|---|
cin | 标准输入流 | 键盘 | istream_withassign | stdin |
cout | 标准输出流 | 屏幕 | ostream_withassign | stdout |
cerr | 标准错误流 | 屏幕 | ostream_withassign | stderr |
clog | 标准错误流 | 屏幕 | ostream_withassign | stderr |
int a = 10, b = 20;
int* p1 = &a;
const int* p2 = &a;//*p2=100(error) p2=&b;(true) 不可以改变指针指向值,但是可以改变指针自身值
int const* p3 = &a;
int* const p4 = &a;//*p4=100;(true) p3=&b;(error) 可以改变指针指向的值,但是不能修改指针自身值
const int* const p5 = &a;//*p5=100;(error) p5=&b;(error)不可以改变指针指向值,也不可以改变指针自身值
int a = 10, b = 20;
const int* p = &a;
int* s0 = p;//编译错误
const int* s1 = p;
int* const s2 = p;//编译错误,const修饰s2,s2的值不能改变,*s2的值可以改变
const int* const s3 = p;
int main()
{
int a = 10;
int* const p = &a;
int* s1 = p;//true
const int* s2 = p;//true
int* const s3 = p;//true
const int* const s4 = p;//true
}
c=a & b; //位与
int* p=&a; //取地址
int& x=a;//引用
引用就是变量的别名
引用错误代码示例(常考)
int main()
{
//容易对空指针引用,程序出现错误
int* ip = NULL;
int a = 10;
ip = &a;//ip里面存放的是a的地址
int& b = *ip;//*ip就是a本身
return 0;
}
常引用是一个万能引用,可以引用一切数值,只能读取,无法修改
const int& c = a;//常引用,c可以读取a的值,但是不能改变a的值
#include
using namespace std;
int main()
{
/*int a = 10, b = 20;
int* s = &a;
const int*& p2 = s;//s的改变会改变p2的值
p2 = &b;
cout << *p2 << endl;
cout << *s << endl;
return 0;*/
int a = 10, b = 20;
int* s = &a;
int* p1 = s;
const int*& p2 = s;
int* const& p3 = s;//s的改变会影响p3的改变,p3的指向值可以改变
const int* const& p4 = s;
/*
int a = 10, b = 20;
int* s = &a;
const int*& p2 = s;
int*& const p3 = s;//修饰引用符号为常性,引用符号为常性
s = &b;
return 0;
*/
}
从语法规则上讲
(3)指针变量存储某个实例(变量或对量)的地址;引用是某个实例的别名
(4)程序要为指针变量分配存储空间;而不为引用分配内存区域
(5)解引用是指针使用时要在前面加“*”;但是引用可以直接使用
(6)指针变量的可以改变,存储不同实例的地址;引用在定义时候就被初始化,之后无法改变(不能是其他实例的引用)
(7)指针变量的值可以为空;没有空引用
(8)对指针变量“ sizeof ”得到的是指针变量的大小(num*type);对引用变量使用“ sizeof ”得到的是变量大小
(9)理论上指针的级数没有限制;但引用只有一级。即不存在引用的引用,但是可以有指针的指针。
(10)引用++与指针++效果不一样。例如就操作而言:引用对元素操作;指针对数组操作,对指针变量的操作,会使指针变量指向下一个实体的地址;对引用操作直接反应到所引用实体
(11)不能对函数中的局部变量或对象以引用或指针方式返回
代码示例
int* func_p()
{
int a = 10;
return &a;//返回的是指针地址,函数结束空间被释放,地址会失效
}
int& func_r()
{
int a = 10;
return a;//返回地址
}
从机器代码上讲
#include
using namespace std;
struct Student
{
char s_id[20];
char s_name[20];
char s_sex[10];
int s_age;
};
void fun_a(struct Student a)//值传递(需要大量时间和空间){}
void fun_b(struct Student* b)//指针(需要判空){}
void fun_c(struct Student& c)//引用(语法糖,形参改变会改变实参){}
void fun_d(const struct Student& d)//常引用(只能读取,不能修改){}
int main()
{
struct Student s1 = { "1901","何小柒","woman",1 };
fun_a(s1);
fun_b(&s1);
fun_c(s1);
fun_d(s1);
return 0;
}
使用常引用或者引用取决于程序本身使用特点,常引用和引用没有好坏之分
#include
using namespace std;
int main()
{
const int a = 10;
int b = 0;
int* p = (int*)&a;//强转去掉a变量的常性(c语言强转)
int* s = const_cast<int*>(&a);//强转去掉a变量的常性(比上面更加明确清楚,c++强转)
int &b=(int&)a;
*p = 100;
b = a;
cout << "a=" << a << "b=" << b << "*p=" << *p << endl;
}
const对引用只有读取功能,没有修改的关系,c++把常变量当作常量来编译
从语义和汇编看
引用
void fun(int& a)
{
int* p = &a;
a = 100;
*p = 200;
}
int main()
{
int x = 10;
int& y = x;
fun(x);
fun(y);
return 0;
}
指针
void fun(int * const a)
{
int* p = a;
*a = 100;
*p = 200;
}
int main()
{
int x = 10;
int* const y = &x;
fun(&x);
fun(y);
return 0;
}
(1)引用不用判空,比指针安全性较高,引用必须给予初始化指向一个已有变量的地址。
(2)引用:自身为常量的指针
(3)将亡值(临时量):失效指针
(4)什么时候函数可以引用返回数值?
此变量的生存期不受函数生存期的影响,函数结束,变量不死亡(静态变量,全局变量,引用)
int Max=100;//全局变量
int& fun(int&x)//引用
{
static int a=10;//静态变量
return x;
}
普通变量可以用常引用也可以用普通引用,但是常变量只能用常引用。
常引用:普通引用不能引用字面常量,字面常量只能用常引用
int main()
{
int a = 10;
const int& b = a;
//底层表示:const int* const b = &a;
const int& c = 10;
//底层表示(分三步走):int tmp=10; const int&c=tmp; const int *const c=&tmp
}
当程序执行函数调用时,系统要建立栈空间,保护现场,传递参数以及控制程序执行的转移等等,这些工作需要系统时间和空间的开销。
在C++中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了inline修饰符,表示为内联函数
当函数功能简单,使用频率很高,为了提高效率,直接将函数的代码嵌入到程序中。但这个办法有缺点,一是相同代码重复书写,,二是程序可读性往往没有使用函数的好。
为了协调好效率和可读性之间的矛盾,C++提供了另一种方法,即定义内联函数,方法是在定义函数时用修饰词inline。
错误代码实例
a.h
inline int my_add(int a,int b);
a.cpp
inline int my_add(int a,int b)
{
return a+b;
}
man.cpp
#include
#include"a.h"
using namespace std;
int main()
{
int z = my_add(12, 23);
}
内联函数的定义和声明不能分开,编译过程中函数体丢失,导致编译失败
内联函数的处理方式是在函数调用点直接展开。在计算机系统下,假如频繁调用就会造成较大的时间开销(现场保护和现场恢复,调用函数开辟栈帧,函数调用完成后栈帧的清除,涉及到中断,陷入到内核)。内联函数的引用减少了函数调用过程中开栈和清栈的开销。
宏的展开
//源代码
return a > b ? a : b ;
//展开代码
(++a) > (b) ? (++a) : (b)
宏的展开具有一些副作用
宏展开具有一些泛型
inline有安全检查和类型检查;
宏没有任何检查机制,只是简单的文本替换。
inline函数是一种更安全的宏(递归函数不适用内联)
如果函数的执行开销小于开栈清栈开销(函数体较小),使用inline处理效率高。如果函数的执行开销大于开栈清栈开销,使用普通函数方式处理。
依赖于编译器
一般情况下,函数调用时的实参个数应与形参相同,但为了更方便地使用函数,C++也允许定义具有缺省参数的函数,这种函数调用时,实参个数可以与形参不相同。
缺省参数指在定义函数时为形参指定缺省值(默认值)。
这样的函数在调用时,对于缺省参数,可以给出实参值,也可以不给出参数值。如果给出实参,将实参传递给形参进行调用,如果不给出实参,则按缺省值进行调用。
缺省参数的函数调用:缺省实参并不一定是常量表达式,可以是任意表达式,甚至可以通过函数调用给出。如果缺省实参是任意表达式,则函数每次被调用时该表达式被重新求值。但表达式必须有意义。
缺省参数不能同时出现在函数的声明和定义中
错误代码示例
缺省参数可以有多个,但所有缺省参数必须放在参数表的右侧,即先定义所有的非缺省参数,再定义缺省参数。这是因为在函数调用时,参数自左向右逐个匹配,当实参和形参个数不一致时只有这样才不会产生二义性。
习惯上,缺省参数在公共头文件包含的函数声明中指定,不要函数的定义中指定。
如果在函数的定义中指定缺省参数值,在公共头文件包含的函数声明中不能再次指定缺省参数值。
缺省实参不—定必须是常量表达式可以使用
任意表达式
函数的默认值参数是在编译时期确认的
函数的原型包括函数返回类型、函数名、形参列表(其中形参名可以省略),且不需要函数体。
为什么c语言不能进行函数重载,c++可以进行函数重载呢?
C语言的名字修饰规则非常简单,_cdecl是C/C++的缺省调用方式,调用约定函数名字前面添加了下划线前缀。
格式: _functionname@number;
格式:@functionname@number;
“C"或者"C++"函数在内部(编译和链接)通过修饰名识别。修饰名是编译器在编译函数定义或者原型时生成的字符串。
修饰名由函数名、类名、调用约定、返回类型、参数等共同决定。
调用约定
_stdcall是Pascal程序的缺省调用方式,通常用于Win32 Api中,函数采用从右到左的压栈方式,自己在退出时清空堆栈。
C调用约定(即用_cdecl关键字说明)按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此,实现可变参数的函数只能使用该调用约定)。
fastcall调用约定是"人"如其名,它的主要特点就是快,因为它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈),在函数名修饰约定方面,它和前两者均不同。
thiscall仅仅应用于”C++“类的成员函数。this指针存放于ECX寄存器,参数从右到左压。thiscall不是关键词,因此不能被程序员指定。
在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预编译(预处理)、编译、汇编、链接。Name Mangling是一种在编译过程中,将函数名、变量名的名字重新命名的机制。
__cdecl调用约定:
1、 以“?"”标识函数名的开始,后跟函数名;
2、 函数名后面以"@@YA"标识参数表的开始,后跟参数表;
3、 参数表以代号表示:
x – void
D – char
E – unsigned char
F-- short
H – int
l – unsigned int
J-- long,
K – unsigned long
M – float,
N – double
_N – bool
PA – 表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以"0"代替,一个"0"代表一次重复;
4、 参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前;
5、 参数表后以"@Z"标识整个名字的结束,如果该函数无参数,则以"Z"标识结束。
模板:解决函数重载的问题。模板是在编译链接时期生成代码
new和delete:delete不是直接删除空间,意思是把new指向的空间还给堆区系统
错误代码示例
(lvalue左值,xvalue僵王值,rvalue右值,prvalue纯右值)
(1)可以取地址的值为左值,不能取地址就为右值,右值引用不可寻址。右值分为字面常量和将亡值。
int a=10;//&a 左值,10 纯右值
int&& b=10;//右值引用
/*底层实现
b=10;
int tmp=10;
int* const b=&tmp;*/
(2)常引用可以引用字面常量
const int& a=10;
(3)右值引用和常引用区别?
#include
using namespace std;
int fun()
{
int a = 10;
return a;
}
int main()
{
int x = fun();
int& b = fun();//
const int& b = fun();//常引用内存空间中的值不可改变
int&& d = fun();//可以改变内存空间存放临时量的地址
return 0;
}
(4)内置类型,自己设计类型class(数据和方法),以及struct结构体类型。struct结构体类型介于两者之间,分为接口(纯虚函数)和数据。【虚拟内存管理】
为什么要设计类型?
优化代码,使程序运行速度加快,减少对内存的赋值和拷贝。
不允许使用memset,当有虚函数的时候,虚函数要指向虚表,就会移动虚表指针。内存拷贝函数谨慎使用。
有虚函数就有构造函数和赋值函数,没有虚函数和继承关系,成员无对象,只转移地址,不产生拷贝构造函数。
有内置类型和自己设计类型,要有拷贝构造函数。
如果没有赋值语句,系统就会产生缺省赋值语句,此语句无函数调用过程,
如果有赋值语句,不会形成现场保护。调动赋值语句,主函数分配栈帧,栈帧分配个赋值语句,不要把对象引用返回。
#include
using namespace std;
//请完成赋值运算符重载
class Object
{
private:
int num;
int ar[5];
public:
Object(int n, int val = 0) :num(n)
{
for (int i = 0; i < n; ++i)
{
ar[i] = val;
}
}
Object(const Object& obj) :num(obj.num)//拷贝构造函数
{
for (int i = 0; i < 5; ++i)
{
ar[i] = obj.ar[i];
}
}
Object& operator=(const Object & obj)//缺省构造函数
{
if (this != &obj)
{
num = obj.num;
for (int i = 0; i < 5; ++i)
{
ar[i] = obj.ar[i];
}
}
return *this;
}
};
int main()
{
Object obja(5, 23);
Object objb(obja);
Object objc(5);
objc = obja;
return 0;
}
为了代码重用,代码就必须是通用的;通用的代码就必须不受数据类型的限制。那么我们可以把数据类型改为一个设计参数。这种类型的程序设计称为参数化(parameterize)程序设计。软件模块由模板(template)构造。包括函数模板(function template)和类模板(class template)。
函数模板可以用来创建一个通用功能的函数,以支持多种不同形参,简化重载函数的设计。函数模板定义如下:
template<模板参数表>
返回类型 函数名(形式参数表)
{
...;//函数体
}
<模板参数表>(template parameter list)尖括号中不能为空,参数可以有多个,用逗号分开。模板参数主要是模板类型参数。
模板类型参数(template type parameter)代表一种类型,由关键字class或 typename(建议typename)后加一个标识符构成,在这里两个关键字的意义相同,它们表示后面的参数名代表一个潜在的内置或用户设计的类型。
编译时期进行代码生成(编译时期的多态)
template<class T>
T Max(T a, T b)
{
return a > b ? a : b;
}
int main()
{
cout << Max(12, 23) << endl;
cout << Max('a', 'b') << endl;
cout << Max(12.23, 34.45) << endl;
return 0;
}
函数模板根据一组实际类型或值构造出独立的函数的过程通常是隐式发生的,称为模板实参推演(template argument deduction)。
在编译过程中,根据函数模板的实参构造出独立的函数,称为模板函数(template function)。构造过程被称为模板实例化( template instantiation)
函数模板可以生成代码
在C++中支持三种域:局部域、名字空间域和类域。
名字空间域是随标准C++而引入的。它相当于一个更加灵活的文件域(全局域),可以用花括号把文件的一部分括起来,并以关键字namespace开头给它起一个名字:
//局部域
int main()
{
int a = 10;
}
//函数域
void fun()
{
int a = 10;
}
//块域
void fun()
{
int a = 10;
{
int b = 20;
}
cout << a << endl;
cout << b << endl; //错误
}
//类域
class A
{
int a = 10;
}
//全局域
int a = 10;
int main()
{
return 0;
}
花括号括起来的部分称声明块。声明块中可以包括:类、变量(带有初始化)、函数(带有定义)等。最外层的名字空间域称为全局名字空间域(global namespace scope),即文件域。
名字空间域的引入,主要是为了解决
全局名字空间污染(global namespace pollution)问题,即防止程序中的全局实体名与其他程序中的全局实体名,命名冲突。
示例
namespace yhp
{
int g_max = 0;
void fun()
{
cout << g_max << endl;
}
}
namespace hxq
{
int g_max = 10;
void fun()
{
cout << g_max << endl;
}
}
int main()
{
yhp::g_max = 10;//作用域限定符限定属于yhp作用域
}