Руководство по c с примечаниями

Руководство по языку программирования Си

Последнее обновление: 06.09.2023

  1. Глава 1. Введение в C

    1. Язык программирования C

    2. Компилятор GCC. Первая программа на Windows

    3. Компилятор Clang. Первая программа на Windows

    4. GCC. Первая программа на Linux

    5. Clang. Первая программа на MacOS

    6. Настройка параметров компиляции

  2. Глава 2. Основы языка Си

    1. Структура программы на Си

    2. Переменные

    3. Типы данных

    4. Консольный вывод. Функция printf

    5. Константы

    6. Арифметические операции

    7. Условные операции

    8. Поразрядные операции

    9. Операции присваивания

    10. Преобразование типов

    11. Условные конструкции

    12. Циклы

    13. Введение в массивы и строки

    14. Ввод в консоли. Функция scanf

  3. Глава 3. Указатели

    1. Что такое указатели

    2. Операции с указателями

    3. Арифметика указателей

    4. Константы и указатели

    5. Указатели, массивы и строки

    6. Массивы указателей и многоуровневая адресация

  4. Глава 4. Функции

    1. Определение и описание функций

    2. Параметры функции

    3. Результат функции

    4. Рекурсивные функции

    5. Область видимости переменных

    6. Внешние объекты

    7. Указатели в параметрах функции

    8. Указатели на функции

    9. Указатели на функции как параметры и результаты функций

    10. Функции с переменным количеством параметров

    11. Параметры командной строки

  5. Глава 5. Препроцессор

    1. Директива #include. Включение файлов

    2. Директива #define

    3. Макросы

    4. Условная компиляция

  6. Глава 6. Структуры

    1. Определение структур

    2. Структуры как элементы структур

    3. Указатели на структуры

    4. Массивы структур

    5. Структуры и функции

    6. Перечисления

    7. Объединения

    8. Битовые поля

  7. Глава 7. Динамическая память

    1. Выделение и освобождение памяти

    2. Выделение памяти для двухмерного массива произвольной длины

    3. Управление динамической памятью

    4. Указатель как результат функции

  8. Глава 8. Ввод-вывод и работа с файлами

    1. Открытие и закрытие потоков

    2. Чтение и запись бинарных файлов

    3. Чтение и запись структур в файл

    4. Чтение и запись в файл и функции fwrite и fread

    5. Чтение и запись текстовых файлов

    6. Форматируемый ввод-вывод

    7. Позиционирование в файле

    8. Консольный ввод-вывод

    9. Форматированный ввод и вывод в строки. Функции sscanf и sprintf

  9. Глава 9. Стандартная библиотека С

    1. Заголовочные файлы стандартной библиотеки С

    2. Работа со строками

    3. Работа с памятью

    4. Работа с датами и временем

    5. Математические функции

    6. Преобразование строк в числа и чисел в строки

    7. Обобщения и макрос _Generic

    8. Поддержка Unicode и кодировки UTF-16 и UTF-32

  10. Глава 10. Среды разработки для С

    1. Первая программа в Visual Studio

    2. Первая программа в Qt Creator

  11. Глава 11. Взаимодействие с кодом Python

    1. Подключение Python

  • Глава 1. Введение в С
    • Язык программирования С
    • Компилятор GCC. Первая программа на Windows
    • Компилятор Clang. Первая программа на Windows
    • GCC. Первая программа на Linux
    • Clang. Первая программа на MacOS
    • Настройка параметров компиляции
  • Глава 2. Основы языка Си
    • Структура программы на Си
    • Переменные
    • Типы данных
    • Консольный вывод. Функция printf
    • Константы
    • Арифметические операции
    • Условные операции
    • Поразрядные операции
    • Операции присваивания
    • Преобразование типов
    • Условные конструкции
    • Циклы
    • Введение в массивы и строки
    • Ввод в консоли. Функция scanf
  • Глава 3. Указатели
    • Что такое указатели
    • Операции с указателями
    • Арифметика указателей
    • Константы и указатели
    • Указатели, массивы и строки
    • Массивы указателей и многоуровневая адресация
  • Глава 4. Функции
    • Определение и описание функций
    • Параметры функции
    • Результат функции
    • Рекурсивные функции
    • Область видимости переменных
    • Внешние объекты
    • Указатели в параметрах функции
    • Указатели на функции
    • Указатели на функции как параметры и результаты функций
    • Функции с переменным количеством параметров
    • Параметры командной строки
  • Глава 5. Препроцессор
    • Директива #include. Включение файлов
    • Директива #define
    • Макросы
    • Условная компиляция
  • Глава 6. Структуры
    • Определение структур
    • Структуры как элементы структур
    • Указатели на структуры
    • Массивы структур
    • Структуры и функции
    • Перечисления
    • Объединения
    • Битовые поля
  • Глава 7. Динамическая память
    • Выделение и освобождение памяти
    • Выделение памяти для двухмерного массива произвольной длины
    • Управление динамической памятью
    • Указатель как результат функции
  • Глава 8. Ввод-вывод и работа с файлами
    • Открытие и закрытие потоков
    • Чтение и запись бинарных файлов
    • Чтение и запись структур в файл
    • Чтение и запись в файл и функции fwrite и fread
    • Чтение и запись текстовых файлов
    • Форматируемый ввод-вывод
    • Позиционирование в файле
    • Консольный ввод-вывод
    • Форматированный ввод и вывод в строки. Функции sscanf и sprintf
  • Глава 9. Стандартная библиотека С
    • Заголовочные файлы стандартной библиотеки С
    • Работа со строками
    • Работа с памятью
    • Работа с датами и временем
    • Математические функции
    • Преобразование строк в числа и чисел в строки
    • Обобщения и макрос _Generic
    • Поддержка Unicode и кодировки UTF-16 и UTF-32
  • Глава 10. Среды разработки для С
    • Первая программа в Visual Studio
    • Первая программа в Qt Creator
  • Глава 11. Взаимодействие с кодом Python
    • Подключение Python

This C Beginner’s Handbook follows the 80/20 rule. You’ll learn 80% of the C programming language in 20% of the time.

This approach will give you a well-rounded overview of the language.

This handbook does not try to cover everything under the sun related to C. It focuses on the core of the language, trying to simplify the more complex topics.

And note: You can get a PDF and ePub version of this C Beginner’s Handbook here.

Enjoy!

Table of Contents

  1. Introduction to C
  2. Variables and types
  3. Constants
  4. Operators
  5. Conditionals
  6. Loops
  7. Arrays
  8. Strings
  9. Pointers
  10. Functions
  11. Input and output
  12. Variables scope
  13. Static variables
  14. Global variables
  15. Type definitions
  16. Enumerated Types
  17. Structures
  18. Command line parameters
  19. Header files
  20. The preprocessor
  21. Conclusion

Introduction to C

C is probably the most widely known programming language. It is used as the reference language for computer science courses all over the world, and it’s probably the language that people learn the most in school along with Python and Java.

I remember it being my second programming language ever, after Pascal.

C is not just what students use to learn programming. It’s not an academic language. And I would say it’s not the easiest language, because C is a rather low level programming language.

Today, C is widely used in embedded devices, and it powers most of the Internet servers, which are built using Linux. The Linux kernel is built using C, and this also means that C powers the core of all Android devices. We can say that C code runs a good portion of the entire world. Right now. Pretty remarkable.

When it was created, C was considered a high level language, because it was portable across machines. Today we kind of take for granted that we can run a program written on a Mac on Windows or Linux, perhaps using Node.js or Python.

Once upon a time, this was not the case at all. What C brought to the table was a language that was simple to implement and that had a compiler that could be easily ported to different machines.

I said compiler: C is a compiled programming language, like Go, Java, Swift or Rust. Other popular programming language like Python, Ruby or JavaScript are interpreted. The difference is consistent: a compiled language generates a binary file that can be directly executed and distributed.

C is not garbage collected. This means we have to manage memory ourselves. It’s a complex task and one that requires a lot of attention to prevent bugs, but it is also what makes C ideal to write programs for embedded devices like Arduino.

C does not hide the complexity and the capabilities of the machine underneath. You have a lot of power, once you know what you can do.

I want to introduce the first C program now, which we’ll call «Hello, World!»

hello.c

#include <stdio.h>

int main(void) {
    printf("Hello, World!");
}

Let’s describe the program source code: we first import the stdio library (the name stands for standard input-output library).

This library gives us access to input/output functions.

C is a very small language at its core, and anything that’s not part of the core is provided by libraries. Some of those libraries are built by normal programmers, and made available for others to use. Some other libraries are built into the compiler. Like stdio and others.

stdio is the library that provides the printf() function.

This function is wrapped into a main() function. The main() function is the entry point of any C program.

But what is a function, anyway?

A function is a routine that takes one or more arguments, and returns a single value.

In the case of main(), the function gets no arguments, and returns an integer. We identify that using the void keyword for the argument, and the int keyword for the return value.

The function has a body, which is wrapped in curly braces. Inside the body we have all the code that the function needs to perform its operations.

The printf() function is written differently, as you can see. It has no return value defined, and we pass a string, wrapped in double quotes. We didn’t specify the type of the argument.

That’s because this is a function invocation. Somewhere, inside the stdio library, printf is defined as

int printf(const char *format, ...);

You don’t need to understand what this means now, but in short, this is the definition. And when we call printf("Hello, World!");, that’s where the function is run.

The main() function we defined above:

#include <stdio.h>

int main(void) {
    printf("Hello, World!");
}

will be run by the operating system when the program is executed.

How do we execute a C program?

As mentioned, C is a compiled language. To run the program we must first compile it. Any Linux or macOS computer already comes with a C compiler built-in. For Windows, you can use the Windows Subsystem for Linux (WSL).

In any case, when you open the terminal window you can type gcc, and this command should return an error saying that you didn’t specify any file:

Screen-Shot-2020-01-29-at-10.10.50

That’s good. It means the C compiler is there, and we can start using it.

Now type the program above into a hello.c file. You can use any editor, but for the sake of simplicity I’m going to use the nano editor in the command line:

Screen-Shot-2020-01-29-at-10.11.39

Type the program:

Screen-Shot-2020-01-29-at-10.16.52

Now press ctrl-X to exit:

Screen-Shot-2020-01-29-at-10.18.11

Confirm by pressing the y key, then press enter to confirm the file name:

Screen-Shot-2020-01-29-at-10.18.15

That’s it, we should be back to the terminal now:

Screen-Shot-2020-01-29-at-10.13.46

Now type

gcc hello.c -o hello

The program should give you no errors:

Screen-Shot-2020-01-29-at-10.16.31

but it should have generated a hello executable. Now type

./hello

to run it:

Screen-Shot-2020-01-29-at-10.19.20

I prepend ./ to the program name to tell the terminal that the command is in the current folder

Awesome!

Now if you call ls -al hello, you can see that the program is only 12KB in size:

Screen-Shot-2020-01-29-at-10.19.55

This is one of the pros of C: it’s highly optimized, and this is also one of the reasons it’s this good for embedded devices that have a very limited amount of resources.

Variables and types

C is a statically typed language.

This means that any variable has an associated type, and this type is known at compilation time.

This is very different than how you work with variables in Python, JavaScript, PHP and other interpreted languages.

When you create a variable in C, you have to specify the type of a variable at the declaration.

In this example we initialize a variable age with type int:

int age;

A variable name can contain any uppercase or lowercase letter, can contain digits and the underscore character, but it can’t start with a  digit. AGE and Age10 are valid variable names, 1age is not.

You can also initialize a variable at declaration, specifying the initial value:

int age = 37;

Once you declare a variable, you are then able to use it in your program code. You can change its value at any time, using the = operator for example, like in age = 100; (provided the new value is of the same type).

In this case:

#include <stdio.h>

int main(void) {
    int age = 0;
    age = 37.2;
    printf("%u", age);
}

the compiler will raise a warning at compile time, and will convert the decimal number to an integer value.

The C built-in data types are int, char, short, long, float, double, long double. Let’s find out more about those.

Integer numbers

C provides us the following types to define integer values:

  • char
  • int
  • short
  • long

Most of the time, you’ll likely use an int to store an integer. But in some cases, you might want to choose one of the other 3 options.

The char type is commonly used to store letters of the ASCII chart, but it can be used to hold small integers from -128 to 127. It takes at least 1 byte.

int takes at least 2 bytes. short takes at least 2 bytes. long takes at least 4 bytes.

As you can see, we are not guaranteed the same values for different environments. We only have an indication. The problem is that the exact numbers that can be stored in each data type depends on the implementation and the architecture.

We’re guaranteed that short is not longer than int. And we’re guaranteed long is not shorter than int.

The ANSI C spec standard determines the minimum values of each type, and thanks to it we can at least know what’s the minimum value we can expect to have at our disposal.

If you are programming C on an Arduino, different board will have different limits.

On an Arduino Uno board, int stores a 2 byte value, ranging from -32,768 to 32,767. On a Arduino MKR 1010, int stores a 4 bytes value, ranging from -2,147,483,648 to 2,147,483,647. Quite a big difference.

On all Arduino boards, short stores a 2 bytes value, ranging from -32,768 to 32,767. long store 4 bytes, ranging from -2,147,483,648 to 2,147,483,647.

Unsigned integers

For all the above data types, we can prepend unsigned to start the range at 0, instead of a negative number. This might make sense in many cases.

  • unsigned char will range from 0 to at least 255
  • unsigned int will range from 0 to at least 65,535
  • unsigned short will range from 0 to at least 65,535
  • unsigned long will range from 0 to at least 4,294,967,295

The problem with overflow

Given all those limits, a question might come up: how can we make sure our numbers do not exceed the limit? And what happens if we do exceed the limit?

If you have an unsigned int number at 255, and you increment it, you’ll get 256 in return. As expected. If you have an unsigned char number at 255, and you increment it, you’ll get 0 in return. It resets starting from the initial possible value.

If you have a unsigned char number at 255 and you add 10 to it, you’ll get the number 9:

#include <stdio.h>

int main(void) {
  unsigned char j = 255;
  j = j + 10;
  printf("%u", j); /* 9 */
}

If you don’t have a signed value, the behavior is undefined. It will basically give you a huge number which can vary, like in this case:

#include <stdio.h>

int main(void) {
  char j = 127;
  j = j + 10;
  printf("%u", j); /* 4294967177 */
}

In other words, C does not protect you from going over the limits of a type. You need to take care of this yourself.

Warnings when declaring the wrong type

When you declare the variable and initialize it with the wrong value, the gcc compiler (the one you’re probably using) should warn you:

#include <stdio.h>

int main(void) {
  char j = 1000;
}
hello.c:4:11: warning: implicit conversion 
  from 'int' to
      'char' changes value from 1000 to -24
      [-Wconstant-conversion]
        char j = 1000;
             ~   ^~~~
1 warning generated.

And it also warns you in direct assignments:

#include <stdio.h>

int main(void) {
  char j;
  j = 1000;
}

But not if you increase the number using, for example, +=:

#include <stdio.h>

int main(void) {
  char j = 0;
  j += 1000;
}

Floating point numbers

Floating point types can represent a much larger set of values than integers can, and can also represent fractions, something that integers can’t do.

Using floating point numbers, we represent numbers as decimal numbers times powers of 10.

You might see floating point numbers written as

  • 1.29e-3
  • -2.3e+5

and in other seemingly weird ways.

The following types:

  • float
  • double
  • long double

are used to represent numbers with decimal points (floating point types). All can represent both positive and negative numbers.

The minimum requirements for any C implementation is that float can represent a range between 10^-37 and 10^+37, and is typically implemented using 32 bits. double can represent a bigger set of numbers. long double can hold even more numbers.

The exact figures, as with integer values, depend on the implementation.

On a modern Mac, a float is represented in 32 bits, and has a precision of 24 significant bits. 8 bits are used to encode the exponent.

A double number is represented in 64 bits, with a precision of 53 significant bits. 11 bits are used to encode the exponent.

The type long double is represented in 80 bits, has a precision of 64 significant bits. 15 bits are used to encode the exponent.

On your specific computer, how can you determine the specific size of the types? You can write a program to do that:

#include <stdio.h>

int main(void) {
  printf("char size: %lu bytes\n", sizeof(char));
  printf("int size: %lu bytes\n", sizeof(int));
  printf("short size: %lu bytes\n", sizeof(short));
  printf("long size: %lu bytes\n", sizeof(long));
  printf("float size: %lu bytes\n", sizeof(float));
  printf("double size: %lu bytes\n", 
    sizeof(double));
  printf("long double size: %lu bytes\n", 
    sizeof(long double));
}

In my system, a modern Mac, it prints:

char size: 1 bytes
int size: 4 bytes
short size: 2 bytes
long size: 8 bytes
float size: 4 bytes
double size: 8 bytes
long double size: 16 bytes

Constants

Let’s now talk about constants.

A constant is declared similarly to variables, except it is prepended with the const keyword, and you always need to specify a value.

Like this:

const int age = 37;

This is perfectly valid C, although it is common to declare constants uppercase, like this:

const int AGE = 37;

It’s just a convention, but one that can greatly help you while reading or writing a C program as it improves readability. Uppercase name means constant, lowercase name means variable.

A constant name follows the same rules for variable names: can contain any uppercase or lowercase letter, can contain digits and the underscore character, but it can’t start with a digit. AGE and Age10 are valid variable names, 1AGE is not.

Another way to define constants is by using this syntax:

#define AGE 37

In this case, you don’t need to add a type, and you don’t also need the = equal sign, and you omit the semicolon at the end.

The C compiler will infer the type from the value specified, at compile time.

Operators

C offers us a wide variety of operators that we can use to operate on data.

In particular, we can identify various groups of operators:

  • arithmetic operators
  • comparison operators
  • logical operators
  • compound assignment operators
  • bitwise operators
  • pointer operators
  • structure operators
  • miscellaneous operators

In this section I’m going to detail all of them, using 2 imaginary variables a and b as examples.

I am keeping bitwise operators, structure operators and pointer operators out of this list, to keep things simpler

Arithmetic operators

In this macro group I am going to separate binary operators and unary operators.

Binary operators work using two operands:

Operator Name Example
= Assignment a = b
+ Addition a + b
- Subtraction a - b
* Multiplication a * b
/ Division a / b
% Modulo a % b

Unary operators only take one operand:

Operator Name Example
+ Unary plus +a
- Unary minus -a
++ Increment a++ or ++a
-- Decrement a-- or --a

The difference between a++ and ++a is that a++ increments the a variable after using it. ++a increments the a variable before using it.

For example:

int a = 2;
int b;
b = a++ /* b is 2, a is 3 */
b = ++a /* b is 4, a is 4 */

The same applies to the decrement operator.

Comparison operators

Operator Name Example
== Equal operator a == b
!= Not equal operator a != b
> Bigger than a > b
< Less than a < b
>= Bigger than or equal to a >= b
<= Less than or equal to a <= b

Logical operators

  • ! NOT (example: !a)
  • && AND (example: a && b)
  • || OR (example: a || b)

Those operators are great when working with boolean values.

Compound assignment operators

Those operators are useful to perform an assignment and at the same time perform an arithmetic operation:

Operator Name Example
+= Addition assignment a += b
-= Subtraction assignment a -= b
*= Multiplication assignment a *= b
/= Division assignment a /= b
%= Modulo assignment a %= b

The ternary operator

The ternary operator is the only operator in C that works with 3 operands, and it’s a short way to express conditionals.

This is how it looks:

<condition> ? <expression> : <expression>

Example:

a ? b : c

If a is evaluated to true, then the b statement is executed, otherwise c is.

The ternary operator is functionality-wise same as an if/else conditional, except it is shorter to express and it can be inlined into an expression.

sizeof

The sizeof operator returns the size of the operand you pass. You can pass a variable, or even a type.

Example usage:

#include <stdio.h>

int main(void) {
  int age = 37;
  printf("%ld\n", sizeof(age));
  printf("%ld", sizeof(int));
}

Operator precedence

With all those operators (and more, which I haven’t covered in this post, including bitwise, structure operators, and pointer operators), we must pay attention when using them together in a single expression.

Suppose we have this operation:

int a = 2;
int b = 4;
int c = b + a * a / b - a;

What’s the value of c? Do we get the addition being executed before the multiplication and the division?

There is a set of rules that help us solve this puzzle.

In order from less precedence to more precedence, we have:

  • the = assignment operator
  • the + and - binary operators
  • the * and / operators
  • the + and - unary operators

Operators also have an associativity rule, which is always left to right except for the unary operators and the assignment.

In:

int c = b + a * a / b - a;

We first execute a * a / b, which, due to being left-to-right, we can separate into a * a and the result / b: 2 * 2 = 4, 4 / 4 = 1.

Then we can perform the sum and the subtraction: 4 + 1 — 2. The value of c is 3.

In all cases, however, I want to make sure you realize you can use parentheses to make any similar expression easier to read and comprehend.

Parentheses have higher priority over anything else.

The above example expression can be rewritten as:

int c = b + ((a * a) / b) - a;

and we don’t have to think about it that much.

Conditionals

Any programming language provides the programmers the ability to perform choices.

We want to do X in some cases, and Y in other cases.

We want to check data, and make choices based on the state of that data.

C provides us 2 ways to do so.

The first is the if statement, with its else helper, and the second is the switch statement.

if

In an if statement, you can check for a condition to be true, and then execute the block provided in the curly brackets:

int a = 1;

if (a == 1) {
  /* do something */
}

You can append an else block to execute a different block if the original condition turns out to be false:

int a = 1;

if (a == 2) {
  /* do something */
} else {
  /* do something else */
}

Beware of one common source of bugs — always use the comparison operator == in comparisons, and not the assignment operator =. If you don’t, the if conditional check will always be true, unless the argument is 0, for example if you do:

int a = 0;

if (a = 0) {
  /* never invoked */
}

Why does this happen? Because the conditional check will look for a boolean result (the result of a comparison), and the 0 number always equates to a false value. Everything else is true, including negative numbers.

You can have multiple else blocks by stacking together multiple if statements:

int a = 1;

if (a == 2) {
  /* do something */
} else if (a == 1) {
  /* do something else */
} else {
  /* do something else again */
}

switch

If you need to do too many if / else / if blocks to perform a check, perhaps because you need to check the exact value of a variable, then switch can be very useful to you.

You can provide a variable as condition, and a series of case entry points for each value you expect:

int a = 1;

switch (a) {
  case 0:
    /* do something */
    break;
  case 1:
    /* do something else */
    break;
  case 2:
    /* do something else */
    break;
}

We need a break keyword at the end of each case to avoid the next case being executed when the one before ends. This «cascade» effect can be useful in some creative ways.

You can add a «catch-all» case at the end, labeled default:

int a = 1;

switch (a) {
  case 0:
    /* do something */
    break;
  case 1:
    /* do something else */
    break;
  case 2:
    /* do something else */
    break;
  default:
    /* handle all the other cases */
    break;
}

Loops

C offers us three ways to perform a loop: for loops, while loops and do while loops. They all allow you to iterate over arrays, but with a few differences. Let’s see them in detail.

For loops

The first and probably most common way to perform a loop is for loops.

Using the for keyword we can define the rules of the loop up front, and then provide the block that is going to be executed repeatedly.

Like this:

for (int i = 0; i <= 10; i++) {
  /* instructions to be repeated */
}

The (int i = 0; i <= 10; i++) block contains 3 parts of the looping details:

  • the initial condition (int i = 0)
  • the test (i <= 10)
  • the increment (i++)

We first define a loop variable, in this case named i. i is a common variable name to be used for loops, along with j for nested loops (a loop inside another loop). It’s just a convention.

The variable is initialized at the 0 value, and the first iteration is done. Then it is incremented as the increment part says (i++ in this case, incrementing by 1), and all the cycle repeats until you get to the number 10.

Inside the loop main block we can access the variable i to know at which iteration we are. This program should print 0 1 2 3 4 5 5 6 7 8 9 10:

for (int i = 0; i <= 10; i++) {
  /* instructions to be repeated */
  printf("%u ", i);
}

Loops can also start from a high number, and go a lower number, like this:

for (int i = 10; i > 0; i--) {
  /* instructions to be repeated */
}

You can also increment the loop variable by 2 or another value:

for (int i = 0; i < 1000; i = i + 30) {
  /* instructions to be repeated */
}

While loops

While loops is simpler to write than a for loop, because it requires a bit more work on your part.

Instead of defining all the loop data up front when you start the loop, like you do in the for loop, using while you just check for a condition:

while (i < 10) {

}

This assumes that i is already defined and initialized with a value.

And this loop will be an infinite loop unless you increment the i variable at some point inside the loop. An infinite loop is bad because it will block the program, allowing nothing else to happen.

This is what you need for a «correct» while loop:

int i = 0;

while (i < 10) {
  /* do something */

  i++;
}

There’s one exception to this, and we’ll see it in one minute. Before, let me introduce do while.

Do while loops

While loops are great, but there might be times when you need to do one particular thing: you want to always execute a block, and then maybe repeat it.

This is done using the do while keyword. In a way it’s very similar to a while loop, but slightly different:

int i = 0;

do {
  /* do something */

  i++;
} while (i < 10);

The block that contains the /* do something */ comment is always executed at least once, regardless of the condition check at the bottom.

Then, until i is less than 10, we’ll repeat the block.

Breaking out of a loop using break

In all the C loops we have a way to break out of a loop at any point  in time, immediately, regardless of the conditions set for the loop.

This is done using the break keyword.

This is useful in many cases. You might want to check for the value of a variable, for example:

for (int i = 0; i <= 10; i++) {
  if (i == 4 && someVariable == 10) {
    break;
  }
}

Having this option to break out of a loop is particularly interesting for while loops (and do while too), because we can create seemingly infinite loops that end when a  condition occurs. You define this inside the loop block:

int i = 0;
while (1) {
  /* do something */

  i++;
  if (i == 10) break;
}

It’s rather common to have this kind of loop in C.

Arrays

An array is a variable that stores multiple values.

Every value in the array, in C, must have the same type. This means you will have arrays of int values, arrays of double values, and more.

You can define an array of int values like this:

int prices[5];

You must always specify the size of the array. C does not provide dynamic arrays out of the box (you have to use a data structure like a linked list for that).

You can use a constant to define the size:

const int SIZE = 5;
int prices[SIZE];

You can initialize an array at definition time, like this:

int prices[5] = { 1, 2, 3, 4, 5 };

But you can also assign a value after the definition, in this way:

int prices[5];

prices[0] = 1;
prices[1] = 2;
prices[2] = 3;
prices[3] = 4;
prices[4] = 5;

Or, more practical, using a loop:

int prices[5];

for (int i = 0; i < 5; i++) {
  prices[i] = i + 1;
}

And you can reference an item in the array by using square brackets after the array variable name, adding an integer to determine the index value. Like this:

prices[0]; /* array item value: 1 */
prices[1]; /* array item value: 2 */

Array indexes start from 0, so an array with 5 items, like the prices array above, will have items ranging from prices[0] to prices[4].

The interesting thing about C arrays is that all elements of an array are stored sequentially, one right after another. Not something that normally happens with higher-level programming languages.

Another interesting thing is this: the variable name of the array, prices in the above example, is a pointer to the first element of the array. As such it can be used like a normal pointer.

More on pointers soon.

Strings

In C, strings are one special kind of array: a string is an array of char values:

char name[7];

I introduced the char type when I introduced types, but in short it is commonly used to store letters of the ASCII chart.

A string can be initialized like you initialize a normal array:

char name[7] = { "F", "l", "a", "v", "i", "o" };

Or more conveniently with a string literal (also called string constant), a sequence of characters enclosed in double quotes:

char name[7] = "Flavio";

You can print a string via printf() using %s:

printf("%s", name);

Do you notice how «Flavio» is 6 chars long, but I defined an array of length 7? Why? This is because the last character in a string must be a  0 value, the string terminator, and we must make space for it.

This is important to keep in mind especially when manipulating strings.

Speaking of manipulating strings, there’s one important standard library that is provided by C: string.h.

This library is essential because it abstracts many of the low level details of working with strings, and provides us with a set of useful functions.

You can load the library in your program by adding on top:

#include <string.h>

And once you do that, you have access to:

  • strcpy() to copy a string over another string
  • strcat() to append a string to another string
  • strcmp() to compare two strings for equality
  • strncmp() to compare the first n characters of two strings
  • strlen() to calculate the length of a string

and many, many more.

Pointers

Pointers are one of the most confusing/challenging parts of C, in my opinion. Especially if you are new to programming, but also if you come from a higher level programming language like Python or JavaScript.

In this section I want to introduce them in the simplest yet not-dumbed-down way possible.

A pointer is the address of a block of memory that contains a variable.

When you declare an integer number like this:

int age = 37;

We can use the & operator to get the value of the address in memory of a variable:

printf("%p", &age); /* 0x7ffeef7dcb9c */

I used the %p format specified in printf() to print the address value.

We can assign the address to a variable:

int *address = &age;

Using int *address in the declaration, we are not declaring an integer variable, but rather a pointer to an integer.

We can use the pointer operator * to get the value of the variable an address is pointing to:

int age = 37;
int *address = &age;
printf("%u", *address); /* 37 */

This time we are using the pointer operator again, but since it’s not a declaration this time it means «the value of the variable this pointer points to».

In this example we declare an age variable, and we use a pointer to initialize the value:

int age;
int *address = &age;
*address = 37;
printf("%u", *address);

When working with C, you’ll find that a lot of things are built on top of this simple concept. So make sure you familiarize with it a bit by running the above examples on your own.

Pointers are a great opportunity because they force us to think about memory addresses and how data is organized.

Arrays are one example. When you declare an array:

int prices[3] = { 5, 4, 3 };

The prices variable is actually a pointer to the first item of the array. You can get the value of the first item using this printf() function in this case:

printf("%u", *prices); /* 5 */

The cool thing is that we can get the second item by adding 1 to the prices pointer:

printf("%u", *(prices + 1)); /* 4 */

And so on for all the other values.

We can also do many nice string manipulation operations, since strings are arrays under the hood.

We also have many more applications, including passing the reference of an object or a function around to avoid consuming more resources to copy it.

Functions

Functions are the way we can structure our code into subroutines that we can:

  1. give a name to
  2. call when we need them

Starting from your very first program, a «Hello, World!», you immediately make use of C functions:

#include <stdio.h>

int main(void) {
    printf("Hello, World!");
}

The main() function is a very important function, as it’s the entry point for a C program.

Here’s another function:

void doSomething(int value) {
    printf("%u", value);
}

Functions have 4 important aspects:

  1. they have a name, so we can invoke («call») them later
  2. they specify a return value
  3. they can have arguments
  4. they have a body, wrapped in curly braces

The function body is the set of instructions that are executed any time we invoke a function.

If the function has no return value, you can use the keyword void before the function name. Otherwise you specify the function return value type (int for an integer, float for a floating point value, const char * for a string, etc).

You cannot return more than one value from a function.

A function can have arguments. They are optional. If it does not have them, inside the parentheses we insert void, like this:

void doSomething(void) {
   /* ... */
}

In this case, when we invoke the function we’ll call it with nothing in the parentheses:

doSomething();

If we have one parameter, we specify the type and the name of the parameter, like this:

void doSomething(int value) {
   /* ... */
}

When we invoke the function, we’ll pass that parameter in the parentheses, like this:

doSomething(3);

We can have multiple parameters, and if so we separate them using a comma, both in the declaration and in the invocation:

void doSomething(int value1, int value2) {
   /* ... */
}

doSomething(3, 4);

Parameters are passed by copy. This means that if you modify value1, its value is modified locally. The value outside of the function, where it was passed in the invocation, does not change.

If you pass a pointer as a parameter, you can modify that variable value because you can now access it directly using its memory address.

You can’t define a default value for a parameter. C++ can do that (and so Arduino Language programs can), but C can’t.

Make sure you define the function before calling it, or the compiler will raise a warning and an error:

➜  ~ gcc hello.c -o hello; ./hello
hello.c:13:3: warning: implicit declaration of
      function 'doSomething' is invalid in C99
      [-Wimplicit-function-declaration]
  doSomething(3, 4);
  ^
hello.c:17:6: error: conflicting types for
      'doSomething'
void doSomething(int value1, char value2) {
     ^
hello.c:13:3: note: previous implicit declaration
      is here
  doSomething(3, 4);
  ^
1 warning and 1 error generated.

The warning you get regards the ordering, which I already mentioned.

The error is about another thing, related. Since C does not «see» the function declaration before the invocation, it must make assumptions. And it assumes the function to return int. The function however returns void, hence the error.

If you change the function definition to:

int doSomething(int value1, int value2) {
  printf("%d %d\n", value1, value2);
  return 1;
}

you’d just get the warning, and not the error:

➜  ~ gcc hello.c -o hello; ./hello
hello.c:14:3: warning: implicit declaration of
      function 'doSomething' is invalid in C99
      [-Wimplicit-function-declaration]
  doSomething(3, 4);
  ^
1 warning generated.

In any case, make sure you declare the function before using it. Either move the function up, or add the function prototype in a header file.

Inside a function, you can declare variables.

void doSomething(int value) {
  int doubleValue = value * 2;
}

A variable is created at the point of invocation of the function and is destroyed when the function ends. It’s not visible from the outside.

Inside a function, you can call the function itself. This is called recursion and it’s something that offers peculiar opportunities.

Input and output

C is a small language, and the «core» of C does not include any Input/Output (I/O) functionality.

This is not something unique to C, of course. It’s common for the language core to be agnostic of I/O.

In the case of C, Input/Output is provided to us by the C Standard Library via a set of functions defined in the stdio.h header file.

You can import this library using:

#include <stdio.h>

on top of your C file.

This library provides us with, among many other functions:

  • printf()
  • scanf()
  • sscanf()
  • fgets()
  • fprintf()

Before describing what those functions do, I want to take a minute to talk about I/O streams.

We have 3 kinds of I/O streams in C:

  • stdin (standard input)
  • stdout (standard output)
  • stderr (standard error)

With I/O functions we always work with streams. A stream is a high level interface that can represent a device or a file. From the C standpoint, we don’t have any difference in reading from a file or reading from the command line: it’s an I/O stream in any case.

That’s one thing to keep in mind.

Some functions are designed to work with a specific stream, like printf(), which we use to print characters to stdout. Using its more general counterpart fprintf(), we can specify which stream to write to.

Since I started talking about printf(), let’s introduce it now.

printf() is one of the first functions you’ll use when learning C programming.

In its simplest usage form, you pass it a string literal:

printf("hey!");

and the program will print the content of the string to the screen.

You can print the value of a variable. But it’s a bit tricky because you need to add a special character, a placeholder, which changes depending on the type of the variable. For example we use %d for a signed decimal integer digit:

int age = 37;

printf("My age is %d", age);

We can print more than one variable by using commas:

int age_yesterday = 37;
int age_today = 36;

printf("Yesterday my age was %d and today is %d", age_yesterday, age_today);

There are other format specifiers like %d:

  • %c for a char
  • %s for a char
  • %f for floating point numbers
  • %p for pointers

and many more.

We can use escape characters in printf(), like \n which we can use to make the output create a new line.

scanf()

printf() is used as an output function. I want to introduce an input function now, so we can say we can do all the I/O thing: scanf().

This function is used to get a value from the user running the program, from the command line.

We must first define a variable that will hold the value we get from the input:

int age;

Then we call scanf() with 2 arguments: the format (type) of the variable, and the address of the variable:

scanf("%d", &age);

If we want to get a string as input, remember that a string name is a pointer to the first character, so you don’t need the & character before it:

char name[20];
scanf("%s", name);

Here’s a little program that uses both printf() and scanf():

#include <stdio.h>

int main(void) {
  char name[20];
  printf("Enter your name: ");
  scanf("%s", name);
  printf("you entered %s", name);
}

Variable scope

When you define a variable in a C program, depending on where you declare it, it will have a different scope.

This means that it will be available in some places, but not in others.

The position determines 2 types of variables:

  • global variables
  • local variables

This is the difference: a variable declared inside a function is a local variable, like this:

int main(void) {
  int age = 37;
}

Local variables are only accessible from within the function, and when the function ends they stop their existence. They are cleared from the memory (with some exceptions).

A variable defined outside a function is a global variable, like in this example:

int age = 37;

int main(void) {
  /* ... */
}

Global variables are accessible from any function of the program, and they are available for the whole execution of the program, until it ends.

I mentioned that local variables are not available any more after the function ends.

The reason is that local variables are declared on the stack, by default, unless you explicitly allocate them on the heap using pointers. But then you have to manage the memory yourself.

Static variables

Inside a function, you can initialize a static variable using the static keyword.

I said «inside a function» because global variables are static by default, so there’s no need to add the keyword.

What’s a static variable? A static variable is initialized to 0 if no initial value is specified, and it retains the value across function calls.

Consider this function:

int incrementAge() {
  int age = 0;
  age++;
  return age;
}

If we call incrementAge() once, we’ll get 1 as the return value. If we call it more than once, we’ll always get 1 back, because age is a local variable and it’s re-initialized to 0 on every single function call.

If we change the function to:

int incrementAge() {
  static int age = 0;
  age++;
  return age;
}

Now every time we call this function, we’ll get an incremented value:

printf("%d\n", incrementAge());
printf("%d\n", incrementAge());
printf("%d\n", incrementAge());

will give us

1
2
3

We can also omit initializing age to 0 in static int age = 0;, and just write static int age; because static variables are automatically set to 0 when created.

We can also have static arrays. In this case, each single item in the array is initialized to 0:

int incrementAge() {
  static int ages[3];
  ages[0]++;
  return ages[0];
}

Global variables

In this section I want to talk more about the difference between global and local variables.

A local variable is defined inside a function, and it’s only available inside that function.

Like this:

#include <stdio.h>

int main(void) {
  char j = 0;
  j += 10;
  printf("%u", j); //10
}

j is not available anywhere outside the main function.

A global variable is defined outside of any function, like this:

#include <stdio.h>

char i = 0;

int main(void) {
  i += 10;
  printf("%u", i); //10
}

A global variable can be accessed by any function in the program.  Access is not limited to reading the value: the variable can be updated by any function.

Due to this, global variables are one way we have of sharing the same data between functions.

The main difference with local variables is that the memory allocated for variables is freed once the function ends.

Global variables are only freed when the program ends.

Type definitions

The typedef keyword in C allows you to defined new types.

Starting from the built-in C types, we can create our own types, using this syntax:

typedef existingtype NEWTYPE

The new type we create is usually, by convention, uppercase.

This it to distinguish it more easily, and immediately recognize it as type.

For example we can define a new NUMBER type that is an int:

typedef int NUMBER

and once you do so, you can define new NUMBER variables:

NUMBER one = 1;

Now you might ask: why? Why not just use the built-in type int instead?

Well, typedef gets really useful when paired with two things: enumerated types and structures.

Enumerated types

Using the typedef and enum keywords we can define a type that can have either one value or another.

It’s one of the most important uses of the typedef keyword.

This is the syntax of an enumerated type:

typedef enum {
  //...values
} TYPENAME;

The enumerated type we create is usually, by convention, uppercase.

Here is a simple example:

typedef enum {
  true,
  false
} BOOLEAN;

C comes with a bool type, so this example is not really practical, but you get the idea.

Another example is to define weekdays:

typedef enum {
  monday,  
  tuesday,
  wednesday,
  thursday,
  friday,
  saturday,
  sunday
} WEEKDAY;

Here’s a simple program that uses this enumerated type:

#include <stdio.h>

typedef enum {
  monday,  
  tuesday,
  wednesday,
  thursday,
  friday,
  saturday,
  sunday
} WEEKDAY;

int main(void) {
  WEEKDAY day = monday;

  if (day == monday) {
    printf("It's monday!"); 
  } else {
    printf("It's not monday"); 
  }
}

Every item in the enum definition is paired to an integer, internally. So in this example monday is 0, tuesday is 1 and so on.

This means the conditional could have been if (day == 0) instead of if (day == monday), but it’s way simpler for us humans to reason with names rather than numbers, so it’s a very convenient syntax.

Structures

Using the struct keyword we can create complex data structures using basic C types.

A structure is a collection of values of different types. Arrays in C are limited to a type, so structures can prove to be very interesting in a lot of use cases.

This is the syntax of a structure:

struct <structname> {
  //...variables
};

Example:

struct person {
  int age;
  char *name;
};

You can declare variables that have as type that structure by adding them after the closing curly bracket, before the semicolon, like this:

struct person {
  int age;
  char *name;
} flavio;

Or multiple ones, like this:

struct person {
  int age;
  char *name;
} flavio, people[20];

In this case I declare a single person variable named flavio, and an array of 20 person named people.

We can also declare variables later on, using this syntax:

struct person {
  int age;
  char *name;
};

struct person flavio;

We can initialize a structure at declaration time:

struct person {
  int age;
  char *name;
};

struct person flavio = { 37, "Flavio" };

and once we have a structure defined, we can access the values in it using a dot:

struct person {
  int age;
  char *name;
};

struct person flavio = { 37, "Flavio" };
printf("%s, age %u", flavio.name, flavio.age);

We can also change the values using the dot syntax:

struct person {
  int age;
  char *name;
};

struct person flavio = { 37, "Flavio" };

flavio.age = 38;

Structures are very useful because we can pass them around as function parameters, or return values, embedding various variables within them. Each variable has a label.

It’s important to note that structures are passed by copy, unless of course you pass a pointer to a struct, in which case it’s passed by reference.

Using typedef we can simplify the code when working with structures.

Let’s look at an example:

typedef struct {
  int age;
  char *name;
} PERSON;

The structure we create using typedef is usually, by convention, uppercase.

Now we can declare new PERSON variables like this:

PERSON flavio;

and we can initialize them at declaration in this way:

PERSON flavio = { 37, "Flavio" };

Command line parameters

In your C programs, you might need to accept parameters from the command line when the command launches.

For simple needs, all you need to do to do so is change the main() function signature from

int main(void)

to

int main (int argc, char *argv[])

argc is an integer number that contains the number of parameters that were provided in the command line.

argv is an array of strings.

When the program starts, we are provided the arguments in those 2 parameters.

Note that there’s always at least one item in the argv array: the name of the program

Let’s take the example of the C compiler we use to run our programs, like this:

gcc hello.c -o hello

If this was our program, we’d have argc being 4 and argv being an array containing

  • gcc
  • hello.c
  • -o
  • hello

Let’s write a program that prints the arguments it receives:

#include <stdio.h>

int main (int argc, char *argv[]) {
  for (int i = 0; i < argc; i++) {
    printf("%s\n", argv[i]);
  }
}

If the name of our program is hello and we run it like this: ./hello, we’d get this as output:

./hello

If we pass some random parameters, like this: ./hello a b c we’d get this output to the terminal:

./hello
a
b
c

This system works great for simple needs. For more complex needs, there are commonly used packages like getopt.

Simple programs can be put in a single file. But when your program grows larger it’s impossible to keep it all in just one file.

You can move parts of a program to a separate file. Then you create a header file.

A header file looks like a normal C file, except it ends with .h instead of .c. Instead of the implementations of your functions and the other parts of a program, it holds the declarations.

You already used header files when you first used the printf() function, or other I/O function, and you had to type:

#include <stdio.h>

to use it.

#include is a preprocessor directive.

The preprocessor goes and looks up the stdio.h file in the standard library because you used brackets around it. To include your own header files, you’ll use quotes, like this:

#include "myfile.h"

The above will look up myfile.h in the current folder.

You can also use a folder structure for libraries:

#include "myfolder/myfile.h"

Let’s look at an example. This program calculates the years since a given year:

#include <stdio.h>

int calculateAge(int year) {
  const int CURRENT_YEAR = 2020;
  return CURRENT_YEAR - year;
}

int main(void) {
  printf("%u", calculateAge(1983));
}

Suppose I want to move the calculateAge function to a separate file.

I create a calculate_age.c file:

int calculateAge(int year) {
  const int CURRENT_YEAR = 2020;
  return CURRENT_YEAR - year;
}

And a calculate_age.h file where I put the function prototype, which is the same as the function in the .c file, except the body:

int calculateAge(int year);

Now in the main .c file we can go and remove the calculateAge() function definition, and we can import calculate_age.h, which will make the calculateAge() function available:

#include <stdio.h>
#include "calculate_age.h"

int main(void) {
  printf("%u", calculateAge(1983));
}

Don’t forget that to compile a program composed by multiple files, you need to list them all in the command line, like this:

gcc -o main main.c calculate_age.c

And with more complex setups, a Makefile is necessary to tell the compiler how to compile the program.

The preprocessor

The preprocessor is a tool that helps us a lot when programming with C. It is part of the C Standard, just like the language, the compiler, and the standard library.

It parses our program and makes sure that the compiler gets all the things it needs before going on with the process.

What does it do, in practice?

For example, it looks up all the header files you include with the #include directive.

It also looks at every constant you defined using #define and substitutes it with its actual value.

That’s just the start. I mentioned those 2 operations because they are the most common ones. The preprocessor can do a lot more.

Did you notice #include and #define have a # at the beginning? That’s common to all the preprocessor directives. If a line starts with #, that’s taken care of by the preprocessor.

Conditionals

One of the things we can do is to use conditionals to change how our program will be compiled, depending on the value of an expression.

For example we can check if the DEBUG constant is 0:

#include <stdio.h>

const int DEBUG = 0;

int main(void) {
#if DEBUG == 0
  printf("I am NOT debugging\n");
#else
  printf("I am debugging\n");
#endif
}

Symbolic constants

We can define a symbolic constant:

#define VALUE 1
#define PI 3.14
#define NAME "Flavio"

When we use NAME or PI or VALUE in our program, the preprocessor replaces its name with the value before executing the program.

Symbolic constants are very useful because we can give names to values without creating variables at compilation time.

Macros

With #define we can also define a macro. The difference between a macro and a symbolic constant is that a macro can accept an argument and typically contains code, while a symbolic constant is a value:

#define POWER(x) ((x) * (x))

Notice the parentheses around the arguments: this is a good practice to avoid issues when the macro is replaced in the precompilation process.

Then we can use it in our code like this:

printf("%u\n", POWER(4)); //16

The big difference with functions is that macros do not specify the type of their arguments or return values, which might be handy in some cases.

Macros, however, are limited to one line definitions.

If defined

We can check if a symbolic constant or a macro is defined using #ifdef:

#include <stdio.h>
#define VALUE 1

int main(void) {
#ifdef VALUE
  printf("Value is defined\n");
#else
  printf("Value is not defined\n");
#endif
}

We also have #ifndev to check for the opposite (macro not defined).

We can also use #if defined and #if !defined to do the same task.

It’s common to wrap some block of code into a block like this:

#if 0

#endif

to temporarily prevent it from running, or to use a DEBUG symbolic constant:

#define DEBUG 0

#if DEBUG
  //code only sent to the compiler
  //if DEBUG is not 0
#endif

Predefined symbolic constants you can use

The preprocessor also defines a number of symbolic constants you can use, identified by the 2 underscores before and after the name, including:

  • __LINE__ translates to the current line in the source code file
  • __FILE__ translates to the name of the file
  • __DATE__ translates to the compilation date, in the Mmm gg aaaa format
  • __TIME__ translates to the compilation time, in the hh:mm:ss format

Conclusion

Thanks a lot for reading this handbook!

I hope it will inspire you to know more about C.

For more tutorials, check out my blog flaviocopes.com.

Send any feedback, errata, or opinions at hey@flaviocopes.com

And remember: You can get a PDF and ePub version of this C Beginner’s Handbook

You can reach me on Twitter @flaviocopes.

Learn to code for free. freeCodeCamp’s open source curriculum has helped more than 40,000 people get jobs as developers. Get started

Предисловие

Я несколько раз в своих комментариях ссылался на книгу Эндрю Таненбаума «Operating Systems Design and Implementation» на ее первое издание и на то, как в ней представлен язык Си. И эти комментарии всегда вызывали интерес. Я решил, что пришло время опубликовать перевод этого введения в язык Си. Оно по-прежнему актуально. Хотя наверняка найдутся и те, кто не слышал о языке программировании PL/1, а может даже и об операционной системе Minix.

Это описание интересно также и с исторической точки зрения и для понимания того, как далеко ушел язык Си с момента своего рождения и IT-отрасль в целом.

Хочу сразу оговориться, что мой второй язык французский:

image

Но это компенсируется 46-летним программистским стажем.
Итак, приступим, наступила очередь Эндрю Таненбаума.

Введение в язык Си (стр. 350 — 362)

Язык программирования Cи был создан Деннисом Ритчи из AT&T Bell Laboratories как язык программирования высокого уровня для разработки операционной системы UNIX. В настоящее время язык широко используется в различных областях. C особенно популярен у системных программистов, потому что позволяет писать программы просто и кратко.

Основной книгой, описывающая язык Cи, является книга Брайана Кернигана и Денниса Ритчи « Язык программирования Cи» (1978). Книги по языку Си писали Bolon (1986), Gehani (1984), Hancock and Krieger (1986), Harbison и Steele (1984) и многие другие.

В этом приложении мы попытаемся дать достаточно полное введение в Cи, так что те кто знаком с языками высокого уровня, такими как Pascal, PL/1 или Modula 2, смогут понять большую часть кода MINIX, приведенного в этой книге. Особенности Cи, которые не используются в MINIX, здесь не обсуждаются. Многочисленные тонкие моменты опущены. Акцент делается на чтении программ на Си, а не на написании кода.

А.1. Основы языка Си

Программа на Cи состоит из набора процедур (часто называемых функциями, даже если они не возвращают значений). Эти процедуры содержат объявления, операторы и другие элементы, которые вместе говорят компьютеру что надо делать. На рисунке A-1 показана небольшая процедура, в которой объявляются три целочисленные переменные и присваиваются им значения. Имя процедуры — main (главная). Процедура не имеет формальных параметров, на что указывает отсутствие каких-либо идентификаторов между скобками за именем процедуры. Тело процедуры заключено в фигурные скобки ( { } ). Этот пример показывает, что Cи имеет переменные, и что эти переменные должны быть объявлены до использования. Cи также имеет операторы, в этом примере это операторы присваивания. Все операторы должны заканчиваться точкой с запятой (в отличие от Паскаля, который использует двоеточия между операторами, а не после них).

Комментарии начинаются с символов « / *» и заканчивается символами «* /» и могут занимать несколько строк.

main ()      /* это комментарий */
{
     int i, j, k; 	         /* объявление 3 целочисленных переменных */
     i  =  10; 	        /* присвоить i значение 10 (десятичное число) */
     j  =  i + 015; 	/* присвоить j значение  i + 015 (восьмеричное число) */
     k = j * j + 0xFF;   /* установить k в j * j + 0xFF (шестнадцатеричное число) */
}
Рис. A-l. Пример процедуры в Си.

Процедура содержит три константы. Константа 10 в первом присваивании
это обычная десятичная константа. Константа 015 является восьмеричной константой
(равно 13 в десятичной системе счисления). Восьмеричные константы всегда начинаются с начального нуля. Константа 0xFF является шестнадцатеричной константой (равной 255 десятичной). Шестнадцатеричные константы всегда начинаются с 0x. Все три типа используются в Cи.

А.2. Основные типы данных

Cи имеет два основных типа данных (переменных): целое и символ, объявляемые как int и char, соответственно. Нет отдельной булевой переменной. В качестве булевой переменной используется переменная int. Если эта переменная содержит 0, то это означает ложь/false, а любое другое значение означает истина/true. Cи также имеет и типы с плавающей точкой, но MINIX не использует их.

К типу int можно применять «прилагательные» short, long или unsigned, которые определяют (зависящий от компилятора) диапазон значений. Большинство процессоров 8088 используют 16-битные целые числа для int и short int и 32-битные целые числа для long int. Целые числа без знака (unsigned int) на процессоре 8088 имеют диапазон от 0 до 65535, а не от -32768 до +32767, как это у обычных целых чисел (int). Символ занимает 8 бит.

Спецификатор register также допускается как для int, так и для char, и является подсказкой для компилятора, что объявленную переменную стоит поместить в регистр, чтобы программа работала быстрее.

Некоторые объявления показаны на рис. А — 2.

int i; 			         /* одно целое число */
short int z1, z2; 	        / *два коротких целых числа */
char c; 			/* один символ */
unsigned short int k; 	/* одно короткое целое без знака */
long flag_poll;	        /* 'int' может быть опущено */
register int r; 		/* переменная регистра */

Рис. А-2. Некоторые объявления.

Преобразование между типами разрешено. Например, оператор

flag_pole = i;

разрешен, даже если i имеет тип int, а flag_pole — long. Во многих случаях
необходимо или полезно принудительно проводить преобразования между типами данных. Для принудительного преобразования достаточно поставить целевой тип в скобках перед выражением для преобразования. Например:

р ( (long) i);

предписывает преобразовать целое число i в long перед передачей его в качестве параметра в процедуру p, которая ожидает именно параметр long.

При преобразовании между типами следует обратить внимание на знак.
При преобразовании символа в целое число некоторые компиляторы обрабатывают символы как знаковые, то есть от — 128 до +127, тогда как другие рассматривают их как
без знака, то есть от 0 до 255. В MINIX часто встречаются такие выражения, как

i = c & 0377;

которые преобразует с (символ) в целое число, а затем выполняет логическое И
(амперсанд) с восьмеричной константой 0377. В результате получается, что старшие 8 бит
устанавливаются в ноль, фактически заставляя рассматривать c как 8-битное число без знака, в диапазоне от 0 до 255.

А.3. Составные типы и указатели

В этом разделе мы рассмотрим четыре способа построения более сложных типов данных: массивы, структуры, объединения и указатели (arrays, structures, unions, and pointers). Массив — это коллекция/множество элементов одного типа. Все массивы в Cи начинаются с элемента 0.

Объявление

int a [10];

объявляет массив a с 10 целыми числами, которые будут хранится в элементах массива от [0] до a [9]. Второе, массивы могут быть трех и более измерений, но они не используются в MINIX.
Структура — это набор переменных, обычно разных типов. Структура в Cи похож на record в Паскале. Оператор

struct {int i; char c;} s;

объявляет s как структуру, содержащую два члена, целое число i и символ c.

Чтобы присвоить члену i структуры s значение 6, нужно записать следующее выражение:

s.i = 6;

где оператор точка указывает, что элемент i принадлежит структуре s.
Объединение — это также набор членов, аналогично структуре, за исключением того, что в любой момент в объединение может находится только один из них. Объявление

union {int i; char c;} u;

означает, что вы можете иметь целое число или символ, но никак не оба. Компилятор должен выделить достаточно места для объединения, чтобы в нем мог разместиться самый большой (с точки зрения занимаемой памяти) элемент объединения. Объединения используются только в двух местах в MINIX (для определения сообщения как объединения нескольких различных структур, и для определения дискового блока как объединения блока данных, блока i-узла, блока каталога и т. д.).

Указатели используются для хранения машинных адресов в Cи. Они используются очень и очень часто. Символ звездочка (*) используется для обозначения указателя в объявлениях. Объявление

int i, *pi, a [10], *b[10], **ppi;

объявляет целое число i, указатель на целое число pi, массив a из 10 элементов, массив b из 10 указателей на целые числа и указатель на указатель ppi на целое число.

Точные правила синтаксиса для сложных объявлений, объединяющих массивы, указатели и другие типы несколько сложны. К счастью, MINIX использует только простые объявления.

На рисунке A-3 показано объявление массива z структур struct table, каждая из которых имеет
три члена, целое число i, указатель cp на символ и символ с.

struct table {	/* каждая структура имеет тип таблицы */
      int i; 		/ *целое число */
      char *cp, c; 	/* указатель на символ и символ */
} z [20]; 		/* это массив из 20 структур */

Рис. А - 3. Массив структур.

Массивы структур распространены в MINIX. Далее, имя table можно объявить как структуру struct table, которую можно использовать в последующих объявлениях. Например,

register struct table *p;

объявляет p указателем на структуру struct table и предлагает сохранить ее
в register. Во время выполнения программы p может указывать, например, на z [4] или
на любой другой элемент в z, все 20 элементов которой являются структурами типа struct table.

Чтобы сделать p указателем на z [4], достаточно написать

p = &z[4];

где амперсанд в качестве унарного (монадического) оператора означает «взять адрес того, что за ним следует ». Скопировать в целочисленную переменную n значение члена i
структуры, на которую указывает указатель р, можно следующим образом:

n = p->i;

Обратите внимание, что стрелка используется для доступа к члену структуры через указатель. Если мы будем использовать переменную z, то тогда мы должны использовать оператор с точкой:

n = z [4] .i;

Разница в том, что z [4] является структурой, и оператор точки выбирает элементы
из составных типов (структуры, массивы) напрямую. С помощью указателей мы не выбираем участника напрямую. Указатель предписывает сначала выбрать структуру и только потом выбрать члена этой структуры.

Иногда удобно дать имя составному типу. Например:

typedef unsigned short int unshort;

определяет unshort как unsigned short (короткое целое число без знака). Теперь unshort может быть использован в программе как основной тип. Например,

unshort ul, *u2, u3[5];

объявляет короткое целое число без знака, указатель на короткое целое число без знака и
массив коротких целых без знака.

А.4. Операторы

Процедуры в Cи содержат объявления и операторы. Мы уже видели объявления, так что теперь мы будем рассматривать операторы. Назначение условного оператора и операторов цикла по существу такие же, как и в других языках. Рисунок А – 4 показывает несколько примеров из них. Единственное, на что стоит обратить внимание, это то, что фигурные скобки используются для группировки операторов, а оператор while имеет две формы, вторая из которых похожа на оператор repeat Паскаля.

Cи также имеет оператор for, но он не похож на оператор for в любом другом языке. Оператор for имеет следующий вид:

for (<инициализация>; <условие>; <выражение>) оператор;

Тоже самое можно выразить через опертор while:

<инициализация>
while(<условие>) {
	<оператор>;
	<выражение>
} 

В качестве примера рассмотрим следующий оператор:

for (i=0; i <n; i = i+l) a[i]=0;

Этот оператор устанавливает первые n элементов массива a равными нулю. Выполнение оператора начинается с установки i в ноль (это делается вне цикла). Затем оператор повторяется до тех пор, пока i < n, выполняя при этом присваивание и увеличение i. Конечно, вместо оператора присвоения значения текущему элементу массива нуля может быть составной оператор (блок), заключенный в фигурные скобки.

if (x < 0) k = 3;        /* простое оператор if */

if (x > y) {	              /* составной оператор if */
     i = 2;
     k = j + l,
}

if (x + 2 <y) {          /* оператор if-else */
      j  = 2;
      k = j - 1;
} else {
      m = 0;
}

while (n > 0) {	      /* оператор while */
     k = k + k;
     n = n - l;
}

do {	         / * другой вид оператора while */
      k = k + k;
       n = n - 1;
} while (n >  0);

Рис. A-4. Примеры операторов if и while в Cи.

Си имеет также оператор аналогичный case-оператору в языке Pascal. Это switch-оператор. Пример представлен на рисунке А-5. В зависимости от значения выражения, указанного в switch, выбирается тот или иной оператор cаse.

Если выражение не соответствует ни одному из операторов case, то выбирается оператор по умолчанию (default).

Если выражение не связано ни с одним оператором case и оператор default отсутствует, то выполнение продолжается со следующего оператора после оператора switch.

Следует отметить, что для выхода из блока case следует использовать оператор break. Если оператор break отсутствует, то будет выполняться следующий блок case.

switch (k) {
      case 10:
            i = 6;
            break;  /* не выполнять case 20, т.е. завершить выполнение опертора switch */
      case 20:
             i = 2;
             k = 4;
             break;	/ * не выполнять default* /
      default:
            j = 5;
}

Рис. A-5. Пример оператора switch

Оператор break также действует внутри циклов for и while. При этом надо помнить, что если оператор break находится внутри серии вложенных циклов, выход осуществляется только на один уровень вверх.

Связанным оператором является оператор continue, который не выходит из цикла,
но вызывает завершение текущей итерации и начало следующей итерации
немедленно. По сути, это возврат к вершине цикла.

Cи имеет процедуры, которые могут вызываться с параметрами или без параметров.
Согласно Кернигану и Ричи (стр. 121), не разрешено передавать массивы,
структуры или процедуры в качестве параметров, хотя передача указателей на все это
допускается. Есть ли книга или нет ее (так и всплывет в памяти:- «Если жизнь на Марсе, нет ли жизни на Марсе»), многие компиляторы языка Си допускают структуры в качестве параметров.
Имя массива, если оно написано без индекса, означает указатель на массив, что упрощает передачу указателя массива. Таким образом, если a является именем массива любого типа, его можно передать в процедуру g, написав

g(а);

Это правило действует только для массивов, на структуры это правило не распространяется.
Процедуры могут возвращать значения, выполняя оператор return. Этот оператор может содержать выражение, результат выполнения которого будет возвращено в качестве значения процедуры, но вызвавшая процедура может смело игнорировать возвращаемое значение. Если процедура возвращает значение, то тип значение записывается перед именем процедуры, как показано на рис. A-6. Аналогично параметрам, процедуры не могут возвращать массивы, структуры или процедуры, но могут вернуть указатели на них. Это правило разработано для более эффективной реализации — все параметры и результаты всегда соответствуют одному машинному слову (в котором хранится адрес). Компиляторы, которые допускают использование структур в качестве параметров, обычно также допускают их использование в качестве возвращаемых значений.

int sum (i, j)        /* эта процедура возвращает целое число */
int i, j ; 	          /*объявление формальных параметров */
{
      return (i + j);       /* добавить параметры и вернуть сумму */
}

Рис. А-6. Пример простой процедуры, которая возвращает значение.

C не имеет встроенных операторов ввода / вывода. Ввод/вывод реализуется путем вызова библиотечных функций, наиболее распространенные из которых проиллюстрированы ниже:

printf («x=% d y = %o z = %x \n», x, y, z);

Первый параметр — это строка символов между кавычками (на самом деле это массив символов).

Любой символ, который не является процентом, просто печатается как есть.

Когда встречается процент, печатается следующий параметр в виде, определяемом буквой, следующей за процентом:

d — вывести в виде десятичного целого числа
o — печатать как восьмеричное целое
u — печатать как беззнаковое десятичное целое
x — печатать как шестнадцатеричное целое
s — печатать как строку символов
c — печатать как один символ

Также допускаются буквы D, 0 и X для десятичной, восьмеричной и шестнадцатеричной печати длинных чисел.

А.5. Выражения

Выражения создаются путем объединения операндов и операторов.

Арифметические операторы, такие как + и -, и реляционные операторы, такие как <
и > похожи на своих аналогов в других языках. Оператор %
используется по модулю. Стоит отметить, что оператор равенства это ==, а оператор неравенства это! =. Чтобы проверить равны ли a и b, можно написать так:

if (a == b) <оператор>;

Си также позволяет объединять оператор присваивания с другими операторами, поэтому

a +=  4;

эквивалентно записи

а = а + 4;

Другие операторы также могут быть объединены таким образом.

Си имеет операторы для манипулирования битами слова. Разрешены как сдвиги, так и побитовые логические операции. Операторы сдвига влево и вправо являются <<
и >> соответственно. Побитовые логические операторы &, | и ^, которые являются логическим И (AND), включающим ИЛИ (OR) и исключающим ИЛИ (XOP) соответственно. Если i имеет значение 035 (восьмеричное), тогда выражение i & 06 имеет значение 04 (восьмеричное). Еще один пример, если i = 7, то

j = (i << 3) | 014;

и получим 074 для j.
Другой важной группой операторов являются унарные операторы, каждый из которых принимает только один операнд. Как унарный оператор, амперсанд & получает адрес переменной.

Если p является указателем на целое число, а i является целым числом, оператор

p = &i;

вычисляет адрес i и сохраняет его в переменной p.
Противоположным взятию адреса является оператор, который принимает указатель в качестве входных данных и вычисляет значение, находящееся по этому адресу. Если мы только что присвоили адрес i указателю p, тогда *p имеет то же значение, что и i.

Другими словами, в качестве унарного оператора за звездочкой следует указатель (или
выражение, дающее указатель), и возвращает значение элемента, на который указывает. Если i имеет значение 6, то оператор

j = *р;

присвоит j число 6.
Оператор! (восклицательный знак – оператор отрицания) возвращает 0, если его операнд отличен от нуля, и 1, если его оператор равен 0.

Он в основном используется в операторах if, например

if (!x) k=0;

проверяет значение х. Если x равен нулю (false), то k присваивается значение 0. В действительности, оператор! отменяет условие, следующее за ним, так же, как оператор not в Паскаль.

Оператор ~ является побитовым оператором дополнения. Каждый 0 в своем операнде
становится 1, а каждый 1 становится 0.

Оператор sizeof сообщает размер его операнда в байтах. Применительно к
массиву из 20 целых чисел a на компьютере с 2-байтовыми целыми числами, например sizeof a будет иметь значение 40.

Последняя группа операторов — это операторы увеличения и уменьшения.

Оператор

р++;

означает увеличение р. На сколько увеличится p, зависит от его типа.
Целые числа или символы увеличиваются на 1, но указатели увеличиваются на
размер объекта, на который указывает Таким образом, если а является массивом структур, а р указатель на одну из этих структур, и мы пишем

p = &a[3];

чтобы заставить p указать на одну из структур в массиве, то после увеличения p
будет указывать на a[4] независимо от того, насколько велики структуры. Оператор

p--;

аналогичен оператору p++, за исключением того, что он уменьшает, а не увеличивает значение операнда.

В операторе

n = k++;

где обе переменные являются целыми числами, исходное значение k присваивается n и
только после этого происходит увеличение k. В операторе

n = ++ k;

сначала увеличивается k, затем его новое значение сохраняется в n.

Таким образом, ++ (или —) оператор может быть записан до или после его операнда, что приводит к получению различных значений.

Последний оператор – это? (знак вопроса), который выбирает одну из двух альтернатив
разделеных двоеточием. Например, оператор,

i = (x < y ? 6 : k + 1);

сравнивает х с у. Если x меньше y, тогда i получает значение 6; в противном случае переменная i получает значение k + 1. Скобки не обязательны.

А.6. Структура программы

Программа на С состоит из одного или нескольких файлов, содержащих процедуры и объявления.
Эти файлы могут быть скомпилированы по отдельности в объектные файлы, которые затем линкуются друг с другом (с помощью компоновщика) для формирования исполняемой программы.
В отличие от Паскаля, объявления процедур не могут быть вложенными, поэтому все они записываются на «верхнем уровне» в файле программы.

Допускается объявлять переменные вне процедур, например, в начале файла перед первым объявлением процедуры. Эти переменные являются глобальными, и могут использоваться в любой процедуре во всей программе, если только ключевое слово static не предшествует объявлению. В этом случае эти переменные нельзя использовать в другом файле. Те же правила применяются к процедурам. Переменные, объявленные внутри процедуры, являются локальными для процедуры.
Процедура может обращаться к целочисленной переменной v, объявленной в другом файле (при условии, что переменная не является статической), объявляя ее у себя внешней:

extern int v;

Каждая глобальная переменная должна быть объявленным ровно один раз без атрибута extern, чтобы выделить память под нее.

Переменные могут быть инициализированы при объявлении:

int size = 100;

Массивы и структуры также могут быть инициализированы. Глобальные переменные, которые не инициализированы явно, получают значение по умолчанию, равное нулю.

А.7. Препроцессор Cи

Прежде чем исходный файл будет передан компилятору Cи, он автоматически обрабатывается
программой под названием препроцессор. Именно выход препроцессора, а не
оригинальная программа, подается на вход компилятора. Препроцессор выполняет
три основных преобразования в файле перед передачей его компилятору:

1. Включение файлов.
2. Определение и замена макросов.
3. Условная компиляция.

Все директивы препроцессора начинаются со знака числа (#) в 1-ом столбце.
Когда директива вида

#include  "prog.h"

встречается препроцессором, он включает файл prog.h, строка за строкой, в
программу, которая будет передана компилятору. Когда директива #include написана как

#include <prog.h>

то включаемый файл ищется в каталоге /usr/include вместо рабочего каталога. В Cи распространена практика группировать объявления, используемые несколькими файлами, в заголовочном файле (обычно с суффиксом .h), и включать их там, где они необходимы.
Препроцессор также позволяет определения макросов. Например,

#define BLOCK_SIZE 1024

определяет макрос BLOCK_SIZE и присваивает ему значение 1024. С этого момента
каждое вхождение строки из 10 символов «BLOCK_SIZE» в файле будет
заменяться 4-символьной строкой «1024» до того, как компилятор увидит файл с программой. По соглашению имена макросов пишутся в верхнем регистре. Макросы могут иметь параметры, но на практике немногие это делают.

Третья особенность препроцессора — условная компиляция. В MINIX есть несколько
мест, где код написан специально для процессора 8088, и этот код не должен включаться при компиляции для другого процессора. Эти разделы выглядят как так:

#ifdef i8088
      <объявления только для 8088>
#endif

Если символ i8088 определен, то операторы между двумя директивами препроцессора #ifdef i8088 и #endif включаются в выходные данные препроцессора; в противном случае они пропускаются. Вызывая компилятор с командой

cc -c -Di8088 prog.c

или включив в программу заявление

#define i8088

мы определяем символ i8088, поэтому весь зависимый код для 8088 быть включен. По мере развития MINIX он может приобрести специальный код для 68000s и других процессоров, которые будут обрабатываться также.

В качестве примера того, как работает препроцессор, рассмотрим программу рис. A-7 (a). Она включает в себя один файл prog.h, содержимое которого выглядит следующим образом:

int x;
#define MAXAELEMENTS 100

Представьте, что компилятор был вызван командой

cc  -E  -Di8088 prog.c

После того, как файл прошел через препроцессор, вывод будет таким, как показано на Рис. A-7 (b).

Именно этот вывод, а не исходный файл, дается как вход в Cи компилятор.

#include prog.h 			        int x;
main ()						main ();
{						{
      int a[MAX_ELEMENTS]; 			   int a [100];
      х = 4;					   х = 4;
      a[x] = 6; 				   а[х] = 6;
#ifdef i8088 					   printf("8088. a[x]:% d\n", a[x]);
      printf ("8088. a[x]:% d\n", a[x]);
#endif						}

#ifdef m68000
      printf ("68000. x=%d\n", x);
#endif
}
            (а) 				     (b)

Рис. А-7. (a) Содержание файла prog.c. (b) Выход препроцессора.

Обратите внимание, что препроцессор выполнил свою работу и удалил все строки, начинающиеся со знаком #. Если компилятор был бы вызван так

cc -c  -Dm68000 prog.c

то была бы включена другая печать. Если бы он был вызван вот так:

cc -c prog.c

то ни одна печать не была бы включена. (Читатель может поразмышлять о том, что случилось бы, если бы компилятор вызывался с обоими флагами -Dflags.)

А.8. Идиомы

В этом разделе мы рассмотрим несколько конструкций, которые характерны для Cи, но не распространены в других языках программирования. Для начала рассмотрим петлю:

while (n--) *p++  =  *q++;

Переменные p и q обычно являются символьными указателями, а n является счетчиком. Цикл копирует n-символьную строку из места, на которое указывает q, в место, на которое указывает р. На каждой итерации цикла счетчик уменьшается, пока он не доходит до 0, и каждый из указателей увеличивается, поэтому они последовательно указывают на ячейки памяти с более высоким номером.

Еще одна распространенная конструкция:

for (i = 0; i < N; i++) a[i] = 0;

которая устанавливает первые N элементов а в 0. Альтернативный способ написания этого цикла выглядит так:

for (p = &a[0]; p < &a[N]; p++) *p = 0;

В этой формулировке целочисленный указатель p инициализируется так, чтобы указывать на нулевой элемент массива. Цикл продолжается до тех пор, пока p не достиг адреса N-ого элемента массива. Конструкция указателя гораздо эффективнее, чем конструкция массива, и поэтому обычно используют ее.

Операторы присвоения могут появляться в неожиданных местах. Например,

if (a = f (x))  < оператор >;

сначала вызывает функцию f, затем присваивает результат вызова функции a и
наконец, проверяет, является ли оно истинным (ненулевым) или ложным (нулевым). Если а не равно нулю, то условие выполнено. Оператор

if (a = b) < оператор >;

также сначало значение переменной b переменной a, а затем проверяет a, не является ли значение ненулевым. И этот оператор полностью отличается от

if (a == b)  < оператор >;

который сравнивает две переменные и выполняет оператор, если они равны.

Послесловие

Вот и все. Вы не поверите, какое я получил огромное удовольствие, готовя этот текст. Как много я вспомнил полезного из того же языка Си. Надеюсь, вы тоже с удовольствием окунетесь в прекрасный мир языка Си.

Предисловие

Я несколько раз в своих комментариях ссылался на книгу Эндрю Таненбаума «Operating Systems Design and Implementation» на ее первое издание и на то, как в ней представлен язык Си. И эти комментарии всегда вызывали интерес. Я решил, что пришло время опубликовать перевод этого введения в язык Си. Оно по-прежнему актуально. Хотя наверняка найдутся и те, кто не слышал о языке программировании PL/1, а может даже и об операционной системе Minix.

Это описание интересно также и с исторической точки зрения и для понимания того, как далеко ушел язык Си с момента своего рождения и IT-отрасль в целом.

Хочу сразу оговориться, что мой второй язык французский:

image

Но это компенсируется 46-летним программистским стажем.
Итак, приступим, наступила очередь Эндрю Таненбаума.

Введение в язык Си (стр. 350 — 362)

Язык программирования Cи был создан Деннисом Ритчи из AT&T Bell Laboratories как язык программирования высокого уровня для разработки операционной системы UNIX. В настоящее время язык широко используется в различных областях. C особенно популярен у системных программистов, потому что позволяет писать программы просто и кратко.

Основной книгой, описывающая язык Cи, является книга Брайана Кернигана и Денниса Ритчи « Язык программирования Cи» (1978). Книги по языку Си писали Bolon (1986), Gehani (1984), Hancock and Krieger (1986), Harbison и Steele (1984) и многие другие.

В этом приложении мы попытаемся дать достаточно полное введение в Cи, так что те кто знаком с языками высокого уровня, такими как Pascal, PL/1 или Modula 2, смогут понять большую часть кода MINIX, приведенного в этой книге. Особенности Cи, которые не используются в MINIX, здесь не обсуждаются. Многочисленные тонкие моменты опущены. Акцент делается на чтении программ на Си, а не на написании кода.

А.1. Основы языка Си

Программа на Cи состоит из набора процедур (часто называемых функциями, даже если они не возвращают значений). Эти процедуры содержат объявления, операторы и другие элементы, которые вместе говорят компьютеру что надо делать. На рисунке A-1 показана небольшая процедура, в которой объявляются три целочисленные переменные и присваиваются им значения. Имя процедуры — main (главная). Процедура не имеет формальных параметров, на что указывает отсутствие каких-либо идентификаторов между скобками за именем процедуры. Тело процедуры заключено в фигурные скобки ( { } ). Этот пример показывает, что Cи имеет переменные, и что эти переменные должны быть объявлены до использования. Cи также имеет операторы, в этом примере это операторы присваивания. Все операторы должны заканчиваться точкой с запятой (в отличие от Паскаля, который использует двоеточия между операторами, а не после них).

Комментарии начинаются с символов « / *» и заканчивается символами «* /» и могут занимать несколько строк.

main ()      /* это комментарий */
{
     int i, j, k; 	         /* объявление 3 целочисленных переменных */
     i  =  10; 	        /* присвоить i значение 10 (десятичное число) */
     j  =  i + 015; 	/* присвоить j значение  i + 015 (восьмеричное число) */
     k = j * j + 0xFF;   /* установить k в j * j + 0xFF (шестнадцатеричное число) */
}
Рис. A-l. Пример процедуры в Си.

Процедура содержит три константы. Константа 10 в первом присваивании
это обычная десятичная константа. Константа 015 является восьмеричной константой
(равно 13 в десятичной системе счисления). Восьмеричные константы всегда начинаются с начального нуля. Константа 0xFF является шестнадцатеричной константой (равной 255 десятичной). Шестнадцатеричные константы всегда начинаются с 0x. Все три типа используются в Cи.

А.2. Основные типы данных

Cи имеет два основных типа данных (переменных): целое и символ, объявляемые как int и char, соответственно. Нет отдельной булевой переменной. В качестве булевой переменной используется переменная int. Если эта переменная содержит 0, то это означает ложь/false, а любое другое значение означает истина/true. Cи также имеет и типы с плавающей точкой, но MINIX не использует их.

К типу int можно применять «прилагательные» short, long или unsigned, которые определяют (зависящий от компилятора) диапазон значений. Большинство процессоров 8088 используют 16-битные целые числа для int и short int и 32-битные целые числа для long int. Целые числа без знака (unsigned int) на процессоре 8088 имеют диапазон от 0 до 65535, а не от -32768 до +32767, как это у обычных целых чисел (int). Символ занимает 8 бит.

Спецификатор register также допускается как для int, так и для char, и является подсказкой для компилятора, что объявленную переменную стоит поместить в регистр, чтобы программа работала быстрее.

Некоторые объявления показаны на рис. А — 2.

int i; 			         /* одно целое число */
short int z1, z2; 	        / *два коротких целых числа */
char c; 			/* один символ */
unsigned short int k; 	/* одно короткое целое без знака */
long flag_poll;	        /* 'int' может быть опущено */
register int r; 		/* переменная регистра */

Рис. А-2. Некоторые объявления.

Преобразование между типами разрешено. Например, оператор

flag_pole = i;

разрешен, даже если i имеет тип int, а flag_pole — long. Во многих случаях
необходимо или полезно принудительно проводить преобразования между типами данных. Для принудительного преобразования достаточно поставить целевой тип в скобках перед выражением для преобразования. Например:

р ( (long) i);

предписывает преобразовать целое число i в long перед передачей его в качестве параметра в процедуру p, которая ожидает именно параметр long.

При преобразовании между типами следует обратить внимание на знак.
При преобразовании символа в целое число некоторые компиляторы обрабатывают символы как знаковые, то есть от — 128 до +127, тогда как другие рассматривают их как
без знака, то есть от 0 до 255. В MINIX часто встречаются такие выражения, как

i = c & 0377;

которые преобразует с (символ) в целое число, а затем выполняет логическое И
(амперсанд) с восьмеричной константой 0377. В результате получается, что старшие 8 бит
устанавливаются в ноль, фактически заставляя рассматривать c как 8-битное число без знака, в диапазоне от 0 до 255.

А.3. Составные типы и указатели

В этом разделе мы рассмотрим четыре способа построения более сложных типов данных: массивы, структуры, объединения и указатели (arrays, structures, unions, and pointers). Массив — это коллекция/множество элементов одного типа. Все массивы в Cи начинаются с элемента 0.

Объявление

int a [10];

объявляет массив a с 10 целыми числами, которые будут хранится в элементах массива от [0] до a [9]. Второе, массивы могут быть трех и более измерений, но они не используются в MINIX.
Структура — это набор переменных, обычно разных типов. Структура в Cи похож на record в Паскале. Оператор

struct {int i; char c;} s;

объявляет s как структуру, содержащую два члена, целое число i и символ c.

Чтобы присвоить члену i структуры s значение 6, нужно записать следующее выражение:

s.i = 6;

где оператор точка указывает, что элемент i принадлежит структуре s.
Объединение — это также набор членов, аналогично структуре, за исключением того, что в любой момент в объединение может находится только один из них. Объявление

union {int i; char c;} u;

означает, что вы можете иметь целое число или символ, но никак не оба. Компилятор должен выделить достаточно места для объединения, чтобы в нем мог разместиться самый большой (с точки зрения занимаемой памяти) элемент объединения. Объединения используются только в двух местах в MINIX (для определения сообщения как объединения нескольких различных структур, и для определения дискового блока как объединения блока данных, блока i-узла, блока каталога и т. д.).

Указатели используются для хранения машинных адресов в Cи. Они используются очень и очень часто. Символ звездочка (*) используется для обозначения указателя в объявлениях. Объявление

int i, *pi, a [10], *b[10], **ppi;

объявляет целое число i, указатель на целое число pi, массив a из 10 элементов, массив b из 10 указателей на целые числа и указатель на указатель ppi на целое число.

Точные правила синтаксиса для сложных объявлений, объединяющих массивы, указатели и другие типы несколько сложны. К счастью, MINIX использует только простые объявления.

На рисунке A-3 показано объявление массива z структур struct table, каждая из которых имеет
три члена, целое число i, указатель cp на символ и символ с.

struct table {	/* каждая структура имеет тип таблицы */
      int i; 		/ *целое число */
      char *cp, c; 	/* указатель на символ и символ */
} z [20]; 		/* это массив из 20 структур */

Рис. А - 3. Массив структур.

Массивы структур распространены в MINIX. Далее, имя table можно объявить как структуру struct table, которую можно использовать в последующих объявлениях. Например,

register struct table *p;

объявляет p указателем на структуру struct table и предлагает сохранить ее
в register. Во время выполнения программы p может указывать, например, на z [4] или
на любой другой элемент в z, все 20 элементов которой являются структурами типа struct table.

Чтобы сделать p указателем на z [4], достаточно написать

p = &z[4];

где амперсанд в качестве унарного (монадического) оператора означает «взять адрес того, что за ним следует ». Скопировать в целочисленную переменную n значение члена i
структуры, на которую указывает указатель р, можно следующим образом:

n = p->i;

Обратите внимание, что стрелка используется для доступа к члену структуры через указатель. Если мы будем использовать переменную z, то тогда мы должны использовать оператор с точкой:

n = z [4] .i;

Разница в том, что z [4] является структурой, и оператор точки выбирает элементы
из составных типов (структуры, массивы) напрямую. С помощью указателей мы не выбираем участника напрямую. Указатель предписывает сначала выбрать структуру и только потом выбрать члена этой структуры.

Иногда удобно дать имя составному типу. Например:

typedef unsigned short int unshort;

определяет unshort как unsigned short (короткое целое число без знака). Теперь unshort может быть использован в программе как основной тип. Например,

unshort ul, *u2, u3[5];

объявляет короткое целое число без знака, указатель на короткое целое число без знака и
массив коротких целых без знака.

А.4. Операторы

Процедуры в Cи содержат объявления и операторы. Мы уже видели объявления, так что теперь мы будем рассматривать операторы. Назначение условного оператора и операторов цикла по существу такие же, как и в других языках. Рисунок А – 4 показывает несколько примеров из них. Единственное, на что стоит обратить внимание, это то, что фигурные скобки используются для группировки операторов, а оператор while имеет две формы, вторая из которых похожа на оператор repeat Паскаля.

Cи также имеет оператор for, но он не похож на оператор for в любом другом языке. Оператор for имеет следующий вид:

for (<инициализация>; <условие>; <выражение>) оператор;

Тоже самое можно выразить через опертор while:

<инициализация>
while(<условие>) {
	<оператор>;
	<выражение>
} 

В качестве примера рассмотрим следующий оператор:

for (i=0; i <n; i = i+l) a[i]=0;

Этот оператор устанавливает первые n элементов массива a равными нулю. Выполнение оператора начинается с установки i в ноль (это делается вне цикла). Затем оператор повторяется до тех пор, пока i < n, выполняя при этом присваивание и увеличение i. Конечно, вместо оператора присвоения значения текущему элементу массива нуля может быть составной оператор (блок), заключенный в фигурные скобки.

if (x < 0) k = 3;        /* простое оператор if */

if (x > y) {	              /* составной оператор if */
     i = 2;
     k = j + l,
}

if (x + 2 <y) {          /* оператор if-else */
      j  = 2;
      k = j - 1;
} else {
      m = 0;
}

while (n > 0) {	      /* оператор while */
     k = k + k;
     n = n - l;
}

do {	         / * другой вид оператора while */
      k = k + k;
       n = n - 1;
} while (n >  0);

Рис. A-4. Примеры операторов if и while в Cи.

Си имеет также оператор аналогичный case-оператору в языке Pascal. Это switch-оператор. Пример представлен на рисунке А-5. В зависимости от значения выражения, указанного в switch, выбирается тот или иной оператор cаse.

Если выражение не соответствует ни одному из операторов case, то выбирается оператор по умолчанию (default).

Если выражение не связано ни с одним оператором case и оператор default отсутствует, то выполнение продолжается со следующего оператора после оператора switch.

Следует отметить, что для выхода из блока case следует использовать оператор break. Если оператор break отсутствует, то будет выполняться следующий блок case.

switch (k) {
      case 10:
            i = 6;
            break;  /* не выполнять case 20, т.е. завершить выполнение опертора switch */
      case 20:
             i = 2;
             k = 4;
             break;	/ * не выполнять default* /
      default:
            j = 5;
}

Рис. A-5. Пример оператора switch

Оператор break также действует внутри циклов for и while. При этом надо помнить, что если оператор break находится внутри серии вложенных циклов, выход осуществляется только на один уровень вверх.

Связанным оператором является оператор continue, который не выходит из цикла,
но вызывает завершение текущей итерации и начало следующей итерации
немедленно. По сути, это возврат к вершине цикла.

Cи имеет процедуры, которые могут вызываться с параметрами или без параметров.
Согласно Кернигану и Ричи (стр. 121), не разрешено передавать массивы,
структуры или процедуры в качестве параметров, хотя передача указателей на все это
допускается. Есть ли книга или нет ее (так и всплывет в памяти:- «Если жизнь на Марсе, нет ли жизни на Марсе»), многие компиляторы языка Си допускают структуры в качестве параметров.
Имя массива, если оно написано без индекса, означает указатель на массив, что упрощает передачу указателя массива. Таким образом, если a является именем массива любого типа, его можно передать в процедуру g, написав

g(а);

Это правило действует только для массивов, на структуры это правило не распространяется.
Процедуры могут возвращать значения, выполняя оператор return. Этот оператор может содержать выражение, результат выполнения которого будет возвращено в качестве значения процедуры, но вызвавшая процедура может смело игнорировать возвращаемое значение. Если процедура возвращает значение, то тип значение записывается перед именем процедуры, как показано на рис. A-6. Аналогично параметрам, процедуры не могут возвращать массивы, структуры или процедуры, но могут вернуть указатели на них. Это правило разработано для более эффективной реализации — все параметры и результаты всегда соответствуют одному машинному слову (в котором хранится адрес). Компиляторы, которые допускают использование структур в качестве параметров, обычно также допускают их использование в качестве возвращаемых значений.

int sum (i, j)        /* эта процедура возвращает целое число */
int i, j ; 	          /*объявление формальных параметров */
{
      return (i + j);       /* добавить параметры и вернуть сумму */
}

Рис. А-6. Пример простой процедуры, которая возвращает значение.

C не имеет встроенных операторов ввода / вывода. Ввод/вывод реализуется путем вызова библиотечных функций, наиболее распространенные из которых проиллюстрированы ниже:

printf («x=% d y = %o z = %x n», x, y, z);

Первый параметр — это строка символов между кавычками (на самом деле это массив символов).

Любой символ, который не является процентом, просто печатается как есть.

Когда встречается процент, печатается следующий параметр в виде, определяемом буквой, следующей за процентом:

d — вывести в виде десятичного целого числа
o — печатать как восьмеричное целое
u — печатать как беззнаковое десятичное целое
x — печатать как шестнадцатеричное целое
s — печатать как строку символов
c — печатать как один символ

Также допускаются буквы D, 0 и X для десятичной, восьмеричной и шестнадцатеричной печати длинных чисел.

А.5. Выражения

Выражения создаются путем объединения операндов и операторов.

Арифметические операторы, такие как + и -, и реляционные операторы, такие как <
и > похожи на своих аналогов в других языках. Оператор %
используется по модулю. Стоит отметить, что оператор равенства это ==, а оператор неравенства это! =. Чтобы проверить равны ли a и b, можно написать так:

if (a == b) <оператор>;

Си также позволяет объединять оператор присваивания с другими операторами, поэтому

a +=  4;

эквивалентно записи

а = а + 4;

Другие операторы также могут быть объединены таким образом.

Си имеет операторы для манипулирования битами слова. Разрешены как сдвиги, так и побитовые логические операции. Операторы сдвига влево и вправо являются <<
и >> соответственно. Побитовые логические операторы &, | и ^, которые являются логическим И (AND), включающим ИЛИ (OR) и исключающим ИЛИ (XOP) соответственно. Если i имеет значение 035 (восьмеричное), тогда выражение i & 06 имеет значение 04 (восьмеричное). Еще один пример, если i = 7, то

j = (i << 3) | 014;

и получим 074 для j.
Другой важной группой операторов являются унарные операторы, каждый из которых принимает только один операнд. Как унарный оператор, амперсанд & получает адрес переменной.

Если p является указателем на целое число, а i является целым числом, оператор

p = &i;

вычисляет адрес i и сохраняет его в переменной p.
Противоположным взятию адреса является оператор, который принимает указатель в качестве входных данных и вычисляет значение, находящееся по этому адресу. Если мы только что присвоили адрес i указателю p, тогда *p имеет то же значение, что и i.

Другими словами, в качестве унарного оператора за звездочкой следует указатель (или
выражение, дающее указатель), и возвращает значение элемента, на который указывает. Если i имеет значение 6, то оператор

j = *р;

присвоит j число 6.
Оператор! (восклицательный знак – оператор отрицания) возвращает 0, если его операнд отличен от нуля, и 1, если его оператор равен 0.

Он в основном используется в операторах if, например

if (!x) k=0;

проверяет значение х. Если x равен нулю (false), то k присваивается значение 0. В действительности, оператор! отменяет условие, следующее за ним, так же, как оператор not в Паскаль.

Оператор ~ является побитовым оператором дополнения. Каждый 0 в своем операнде
становится 1, а каждый 1 становится 0.

Оператор sizeof сообщает размер его операнда в байтах. Применительно к
массиву из 20 целых чисел a на компьютере с 2-байтовыми целыми числами, например sizeof a будет иметь значение 40.

Последняя группа операторов — это операторы увеличения и уменьшения.

Оператор

р++;

означает увеличение р. На сколько увеличится p, зависит от его типа.
Целые числа или символы увеличиваются на 1, но указатели увеличиваются на
размер объекта, на который указывает Таким образом, если а является массивом структур, а р указатель на одну из этих структур, и мы пишем

p = &a[3];

чтобы заставить p указать на одну из структур в массиве, то после увеличения p
будет указывать на a[4] независимо от того, насколько велики структуры. Оператор

p--;

аналогичен оператору p++, за исключением того, что он уменьшает, а не увеличивает значение операнда.

В операторе

n = k++;

где обе переменные являются целыми числами, исходное значение k присваивается n и
только после этого происходит увеличение k. В операторе

n = ++ k;

сначала увеличивается k, затем его новое значение сохраняется в n.

Таким образом, ++ (или —) оператор может быть записан до или после его операнда, что приводит к получению различных значений.

Последний оператор – это? (знак вопроса), который выбирает одну из двух альтернатив
разделеных двоеточием. Например, оператор,

i = (x < y ? 6 : k + 1);

сравнивает х с у. Если x меньше y, тогда i получает значение 6; в противном случае переменная i получает значение k + 1. Скобки не обязательны.

А.6. Структура программы

Программа на С состоит из одного или нескольких файлов, содержащих процедуры и объявления.
Эти файлы могут быть скомпилированы по отдельности в объектные файлы, которые затем линкуются друг с другом (с помощью компоновщика) для формирования исполняемой программы.
В отличие от Паскаля, объявления процедур не могут быть вложенными, поэтому все они записываются на «верхнем уровне» в файле программы.

Допускается объявлять переменные вне процедур, например, в начале файла перед первым объявлением процедуры. Эти переменные являются глобальными, и могут использоваться в любой процедуре во всей программе, если только ключевое слово static не предшествует объявлению. В этом случае эти переменные нельзя использовать в другом файле. Те же правила применяются к процедурам. Переменные, объявленные внутри процедуры, являются локальными для процедуры.
Процедура может обращаться к целочисленной переменной v, объявленной в другом файле (при условии, что переменная не является статической), объявляя ее у себя внешней:

extern int v;

Каждая глобальная переменная должна быть объявленным ровно один раз без атрибута extern, чтобы выделить память под нее.

Переменные могут быть инициализированы при объявлении:

int size = 100;

Массивы и структуры также могут быть инициализированы. Глобальные переменные, которые не инициализированы явно, получают значение по умолчанию, равное нулю.

А.7. Препроцессор Cи

Прежде чем исходный файл будет передан компилятору Cи, он автоматически обрабатывается
программой под названием препроцессор. Именно выход препроцессора, а не
оригинальная программа, подается на вход компилятора. Препроцессор выполняет
три основных преобразования в файле перед передачей его компилятору:

1. Включение файлов.
2. Определение и замена макросов.
3. Условная компиляция.

Все директивы препроцессора начинаются со знака числа (#) в 1-ом столбце.
Когда директива вида

#include  "prog.h"

встречается препроцессором, он включает файл prog.h, строка за строкой, в
программу, которая будет передана компилятору. Когда директива #include написана как

#include <prog.h>

то включаемый файл ищется в каталоге /usr/include вместо рабочего каталога. В Cи распространена практика группировать объявления, используемые несколькими файлами, в заголовочном файле (обычно с суффиксом .h), и включать их там, где они необходимы.
Препроцессор также позволяет определения макросов. Например,

#define BLOCK_SIZE 1024

определяет макрос BLOCK_SIZE и присваивает ему значение 1024. С этого момента
каждое вхождение строки из 10 символов «BLOCK_SIZE» в файле будет
заменяться 4-символьной строкой «1024» до того, как компилятор увидит файл с программой. По соглашению имена макросов пишутся в верхнем регистре. Макросы могут иметь параметры, но на практике немногие это делают.

Третья особенность препроцессора — условная компиляция. В MINIX есть несколько
мест, где код написан специально для процессора 8088, и этот код не должен включаться при компиляции для другого процессора. Эти разделы выглядят как так:

#ifdef i8088
      <объявления только для 8088>
#endif

Если символ i8088 определен, то операторы между двумя директивами препроцессора #ifdef i8088 и #endif включаются в выходные данные препроцессора; в противном случае они пропускаются. Вызывая компилятор с командой

cc -c -Di8088 prog.c

или включив в программу заявление

#define i8088

мы определяем символ i8088, поэтому весь зависимый код для 8088 быть включен. По мере развития MINIX он может приобрести специальный код для 68000s и других процессоров, которые будут обрабатываться также.

В качестве примера того, как работает препроцессор, рассмотрим программу рис. A-7 (a). Она включает в себя один файл prog.h, содержимое которого выглядит следующим образом:

int x;
#define MAXAELEMENTS 100

Представьте, что компилятор был вызван командой

cc  -E  -Di8088 prog.c

После того, как файл прошел через препроцессор, вывод будет таким, как показано на Рис. A-7 (b).

Именно этот вывод, а не исходный файл, дается как вход в Cи компилятор.

#include prog.h 			        int x;
main ()						main ();
{						{
      int a[MAX_ELEMENTS]; 			   int a [100];
      х = 4;					   х = 4;
      a[x] = 6; 				   а[х] = 6;
#ifdef i8088 					   printf("8088. a[x]:% dn", a[x]);
      printf ("8088. a[x]:% dn", a[x]);
#endif						}

#ifdef m68000
      printf ("68000. x=%dn", x);
#endif
}
            (а) 				     (b)

Рис. А-7. (a) Содержание файла prog.c. (b) Выход препроцессора.

Обратите внимание, что препроцессор выполнил свою работу и удалил все строки, начинающиеся со знаком #. Если компилятор был бы вызван так

cc -c  -Dm68000 prog.c

то была бы включена другая печать. Если бы он был вызван вот так:

cc -c prog.c

то ни одна печать не была бы включена. (Читатель может поразмышлять о том, что случилось бы, если бы компилятор вызывался с обоими флагами -Dflags.)

А.8. Идиомы

В этом разделе мы рассмотрим несколько конструкций, которые характерны для Cи, но не распространены в других языках программирования. Для начала рассмотрим петлю:

while (n--) *p++  =  *q++;

Переменные p и q обычно являются символьными указателями, а n является счетчиком. Цикл копирует n-символьную строку из места, на которое указывает q, в место, на которое указывает р. На каждой итерации цикла счетчик уменьшается, пока он не доходит до 0, и каждый из указателей увеличивается, поэтому они последовательно указывают на ячейки памяти с более высоким номером.

Еще одна распространенная конструкция:

for (i = 0; i < N; i++) a[i] = 0;

которая устанавливает первые N элементов а в 0. Альтернативный способ написания этого цикла выглядит так:

for (p = &a[0]; p < &a[N]; p++) *p = 0;

В этой формулировке целочисленный указатель p инициализируется так, чтобы указывать на нулевой элемент массива. Цикл продолжается до тех пор, пока p не достиг адреса N-ого элемента массива. Конструкция указателя гораздо эффективнее, чем конструкция массива, и поэтому обычно используют ее.

Операторы присвоения могут появляться в неожиданных местах. Например,

if (a = f (x))  < оператор >;

сначала вызывает функцию f, затем присваивает результат вызова функции a и
наконец, проверяет, является ли оно истинным (ненулевым) или ложным (нулевым). Если а не равно нулю, то условие выполнено. Оператор

if (a = b) < оператор >;

также сначало значение переменной b переменной a, а затем проверяет a, не является ли значение ненулевым. И этот оператор полностью отличается от

if (a == b)  < оператор >;

который сравнивает две переменные и выполняет оператор, если они равны.

Послесловие

Вот и все. Вы не поверите, какое я получил огромное удовольствие, готовя этот текст. Как много я вспомнил полезного из того же языка Си. Надеюсь, вы тоже с удовольствием окунетесь в прекрасный мир языка Си.

Язык C – Обзор

C – это язык высокого уровня общего назначения, который первоначально был разработан Деннисом М. Ричи для разработки операционной системы UNIX в Bell Labs. Первоначально C был впервые реализован на компьютере DEC PDP-11 в 1972 году.

В 1978 году Брайан Керниган и Деннис Ритчи выпустили первое общедоступное описание C, теперь известное как стандарт K & R.

Операционная система UNIX, компилятор C и, по существу, все прикладные программы UNIX были написаны на C. Теперь C стал широко используемым профессиональным языком по разным причинам –

  • Легко обучаема
  • Структурированный язык
  • Производит эффективные программы
  • Это может обращаться с действиями низкого уровня
  • Он может быть скомпилирован на различных компьютерных платформах

Факты о С

  • C был изобретен для написания операционной системы под названием UNIX.

  • C является преемником языка B, который был представлен в начале 1970-х годов.

  • Язык был официально оформлен в 1988 году Американским национальным институтом стандартов (ANSI).

  • ОС UNIX была полностью написана на C.

  • На сегодняшний день C является наиболее широко используемым и популярным языком системного программирования.

  • Большая часть современного программного обеспечения была реализована с использованием C.

  • Самые популярные на сегодняшний день ОС Linux и RDBMS MySQL написаны на языке C.

C был изобретен для написания операционной системы под названием UNIX.

C является преемником языка B, который был представлен в начале 1970-х годов.

Язык был официально оформлен в 1988 году Американским национальным институтом стандартов (ANSI).

ОС UNIX была полностью написана на C.

На сегодняшний день C является наиболее широко используемым и популярным языком системного программирования.

Большая часть современного программного обеспечения была реализована с использованием C.

Самые популярные на сегодняшний день ОС Linux и RDBMS MySQL написаны на языке C.

Зачем использовать C?

Изначально C использовался для разработки систем, особенно программ, составляющих операционную систему. C был принят как язык разработки системы, потому что он производит код, который работает почти так же быстро, как код, написанный на языке ассемблера. Некоторые примеры использования C могут быть:

  • Операционные системы
  • Компиляторы языка
  • Монтажники
  • Текстовые редакторы
  • Спулеры печати
  • Сетевые драйверы
  • Современные программы
  • Базы данных
  • Переводчики
  • коммунальные услуги

C Программы

Программа переменного тока может варьироваться от 3 до миллионов строк, и ее следует записать в один или несколько текстовых файлов с расширением «.c» ; например, hello.c . Вы можете использовать «vi» , «vim» или любой другой текстовый редактор, чтобы записать вашу C-программу в файл.

В этом руководстве предполагается, что вы знаете, как редактировать текстовый файл и как писать исходный код в программном файле.

C – Настройка среды

Настройка локальной среды

Если вы хотите настроить свою среду для языка программирования C, вам понадобятся следующие два программных инструмента, доступных на вашем компьютере: (а) текстовый редактор и (б) компилятор C.

Текстовый редактор

Это будет использоваться для ввода вашей программы. Примеры нескольких редакторов: Блокнот Windows, Команда редактирования ОС, Бриф, Эпсилон, EMACS и vim или vi.

Название и версия текстовых редакторов могут различаться в разных операционных системах. Например, Блокнот будет использоваться в Windows, а vim или vi могут использоваться как в Windows, так и в Linux или UNIX.

Файлы, которые вы создаете в редакторе, называются исходными файлами и содержат исходные коды программы. Исходные файлы для программ на C обычно называются с расширением ” .c “.

Перед началом программирования убедитесь, что у вас есть один текстовый редактор, и у вас достаточно опыта, чтобы написать компьютерную программу, сохранить ее в файле, скомпилировать и, наконец, выполнить.

Компилятор C

Исходный код, написанный в исходном файле, является удобочитаемым исходным кодом для вашей программы. Он должен быть «скомпилирован» на машинном языке, чтобы ваш процессор мог фактически выполнить программу согласно приведенным инструкциям.

Компилятор компилирует исходные коды в конечные исполняемые программы. Наиболее часто используемым и бесплатным доступным компилятором является компилятор GNU C / C ++, в противном случае вы можете иметь компиляторы из HP или Solaris, если у вас есть соответствующие операционные системы.

В следующем разделе объясняется, как установить компилятор GNU C / C ++ в различных ОС. Мы продолжаем упоминать C / C ++ вместе, потому что компилятор GNU gcc работает как для языков программирования C, так и для C ++.

Установка в UNIX / Linux

Если вы используете Linux или UNIX , проверьте, установлен ли GCC в вашей системе, введя следующую команду из командной строки:

$ gcc -v

Если на вашем компьютере установлен компилятор GNU, он должен напечатать сообщение следующим образом:

Using built-in specs.
Target: i386-redhat-linux
Configured with: ../configure --prefix=/usr .......
Thread model: posix
gcc version 4.1.2 20080704 (Red Hat 4.1.2-46)

Если GCC не установлен, вам придется установить его самостоятельно, используя подробные инструкции, доступные по адресу https://gcc.gnu.org/install/.

Это руководство было написано на основе Linux, и все приведенные примеры были скомпилированы на основе Cent OS системы Linux.

Установка в Mac OS

Если вы используете Mac OS X, самый простой способ получить GCC – это загрузить среду разработки Xcode с веб-сайта Apple и следовать простым инструкциям по установке. После настройки Xcode вы сможете использовать компилятор GNU для C / C ++.

Xcode в настоящее время доступен по адресу developer.apple.com/technologies/tools/ .

Установка на Windows

Чтобы установить GCC в Windows, вам необходимо установить MinGW. Чтобы установить MinGW, перейдите на домашнюю страницу MinGW www.mingw.org и перейдите по ссылке на страницу загрузки MinGW. Загрузите последнюю версию программы установки MinGW, которая должна называться MinGW- <версия> .exe.

При установке Min GW, как минимум, вы должны установить gcc-core, gcc-g ++, binutils и среду выполнения MinGW, но вы можете установить больше.

Добавьте подкаталог bin вашей установки MinGW в переменную среды PATH , чтобы вы могли указывать эти инструменты в командной строке по их простым именам.

После завершения установки вы сможете запустить gcc, g ++, ar, ranlib, dlltool и несколько других инструментов GNU из командной строки Windows.

C – Структура программы

Прежде чем мы изучим основные строительные блоки языка программирования C, давайте рассмотрим минимальную минимальную структуру программы на C, чтобы мы могли взять ее в качестве справочного материала в следующих главах.

Пример Hello World

Программа AC в основном состоит из следующих частей –

  • Команды препроцессора
  • функции
  • переменные
  • Заявления и выражения
  • Комментарии

Давайте посмотрим на простой код, который будет печатать слова «Hello World» –

Live Demo

#include <stdio.h>

int main() {
   /* my first program in C */
   printf("Hello, World! n");
   
   return 0;
}

Давайте посмотрим на различные части вышеуказанной программы –

  • Первая строка программы #include <stdio.h> – это команда препроцессора, которая указывает компилятору C включить файл stdio.h перед переходом к фактической компиляции.

  • Следующая строка int main () – это основная функция, с которой начинается выполнение программы.

  • Следующая строка /*…*/ будет игнорироваться компилятором, и она была добавлена ​​для добавления дополнительных комментариев в программу. Поэтому такие строки называются комментариями в программе.

  • Следующая строка printf (…) – это еще одна функция, доступная в C, которая вызывает сообщение “Hello, World!” быть отображенным на экране.

  • Следующая строка возвращает 0; завершает функцию main () и возвращает значение 0.

Первая строка программы #include <stdio.h> – это команда препроцессора, которая указывает компилятору C включить файл stdio.h перед переходом к фактической компиляции.

Следующая строка int main () – это основная функция, с которой начинается выполнение программы.

Следующая строка /*…*/ будет игнорироваться компилятором, и она была добавлена ​​для добавления дополнительных комментариев в программу. Поэтому такие строки называются комментариями в программе.

Следующая строка printf (…) – это еще одна функция, доступная в C, которая вызывает сообщение “Hello, World!” быть отображенным на экране.

Следующая строка возвращает 0; завершает функцию main () и возвращает значение 0.

Скомпилируйте и выполните программу C

Давайте посмотрим, как сохранить исходный код в файле и как скомпилировать и запустить его. Ниже приведены простые шаги –

  • Откройте текстовый редактор и добавьте вышеупомянутый код.

  • Сохраните файл как hello.c

  • Откройте командную строку и перейдите в каталог, где вы сохранили файл.

  • Введите gcc hello.c и нажмите enter, чтобы скомпилировать ваш код.

  • Если в вашем коде нет ошибок, командная строка переместит вас на следующую строку и сгенерирует исполняемый файл .out .

  • Теперь введите a.out для выполнения вашей программы.

  • Вы увидите вывод «Hello World», напечатанный на экране.

Откройте текстовый редактор и добавьте вышеупомянутый код.

Сохраните файл как hello.c

Откройте командную строку и перейдите в каталог, где вы сохранили файл.

Введите gcc hello.c и нажмите enter, чтобы скомпилировать ваш код.

Если в вашем коде нет ошибок, командная строка переместит вас на следующую строку и сгенерирует исполняемый файл .out .

Теперь введите a.out для выполнения вашей программы.

Вы увидите вывод «Hello World», напечатанный на экране.

$ gcc hello.c
$ ./a.out
Hello, World!

Убедитесь, что компилятор gcc находится в вашем пути и вы запускаете его в каталоге, содержащем исходный файл hello.c.

C – Базовый синтаксис

Вы видели базовую структуру C-программы, поэтому вам будет легко понять другие основные строительные блоки языка C-программирования.

Жетоны в С

Программа AC состоит из различных токенов, и токен является ключевым словом, идентификатором, константой, строковым литералом или символом. Например, следующий оператор C состоит из пяти токенов –

printf("Hello, World! n");

Отдельные токены –

printf
(
"Hello, World! n"
)
;

Точка с запятой

В программе на Си точка с запятой – это терминатор оператора. То есть каждое отдельное утверждение должно заканчиваться точкой с запятой. Это указывает на конец одного логического объекта.

Ниже приведены два разных утверждения –

printf("Hello, World! n");
return 0;

Комментарии

Комментарии подобны тексту помощи в вашей C-программе и игнорируются компилятором. Они начинаются с / * и заканчиваются символами * /, как показано ниже –

/* my first program in C */

Вы не можете иметь комментарии в комментариях, и они не встречаются в строке или символьных литералах.

Идентификаторы

Идентификатор AC – это имя, используемое для идентификации переменной, функции или любого другого определенного пользователем элемента. Идентификатор начинается с буквы от A до Z, от a до z или подчеркивания ‘_’, за которым следуют ноль или более букв, подчеркиваний и цифр (от 0 до 9).

C не допускает использование знаков препинания, таких как @, $ и% в идентификаторах. C – чувствительный к регистру язык программирования. Таким образом, рабочая сила и рабочая сила – два разных идентификатора в C. Вот несколько примеров допустимых идентификаторов:

mohd       zara    abc   move_name  a_123
myname50   _temp   j     a23b9      retVal

Ключевые слова

В следующем списке показаны зарезервированные слова в C. Эти зарезервированные слова не могут использоваться в качестве констант или переменных или любых других имен идентификаторов.

авто еще долго переключатель
перерыв перечисление регистр ЬурейеЕ
дело внешний вернуть союз
голец поплавок короткая неподписанный
Const за подписанный недействительным
Продолжить идти к размер летучий
дефолт если статический в то время как
делать ИНТ структура _Packed
двойной

Пробел в C

Строка, содержащая только пробел, возможно, с комментарием, называется пустой строкой, и компилятор C полностью игнорирует ее.

Пробел – это термин, используемый в C для описания пробелов, вкладок, символов новой строки и комментариев. Пробелы отделяют одну часть оператора от другой и позволяют компилятору определить, где заканчивается один элемент в выражении, например int, и начинается следующий элемент. Поэтому в следующем утверждении –

int age;

должен быть хотя бы один пробельный символ (обычно пробел) между int и age, чтобы компилятор мог их различать. С другой стороны, в следующем утверждении –

fruit = apples + oranges;   // get the total fruit

между фруктами и = или между = и яблоками не нужно вводить пробельные символы, хотя вы можете включить некоторые из них, если хотите улучшить читаемость.

C – Типы данных

Типы данных в c относятся к обширной системе, используемой для объявления переменных или функций различных типов. Тип переменной определяет, сколько места она занимает в хранилище и как интерпретируется сохраненный битовый шаблон.

Типы в C могут быть классифицированы следующим образом:

Sr.No. Типы и описание
1

Основные типы

Они являются арифметическими типами и далее подразделяются на: (а) целочисленные типы и (б) типы с плавающей точкой.

2

Перечисляемые типы

Они снова являются арифметическими типами и используются для определения переменных, которые могут назначать только определенные дискретные целочисленные значения по всей программе.

3

Тип пустоты

Спецификатор типа void указывает, что значение недоступно.

4

Производные типы

Они включают (a) типы указателей, (b) типы массивов, (c) типы структур, (d) типы объединений и (e) типы функций.

Основные типы

Они являются арифметическими типами и далее подразделяются на: (а) целочисленные типы и (б) типы с плавающей точкой.

Перечисляемые типы

Они снова являются арифметическими типами и используются для определения переменных, которые могут назначать только определенные дискретные целочисленные значения по всей программе.

Тип пустоты

Спецификатор типа void указывает, что значение недоступно.

Производные типы

Они включают (a) типы указателей, (b) типы массивов, (c) типы структур, (d) типы объединений и (e) типы функций.

Типы массивов и типы структур совместно называются агрегатными типами. Тип функции указывает тип возвращаемого значения функции. Мы увидим основные типы в следующем разделе, где другие типы будут рассмотрены в следующих главах.

Целочисленные типы

В следующей таблице приведены сведения о стандартных целочисленных типах с их размерами хранения и диапазонами значений.

Тип Размер хранилища Диапазон значений
голец 1 байт От -128 до 127 или от 0 до 255
без знака 1 байт От 0 до 255
подписанный символ 1 байт От -128 до 127
ИНТ 2 или 4 байта От -32 768 до 32 767 или от -2 147 483 648 до 2 147 483 647
без знака int 2 или 4 байта От 0 до 65 535 или от 0 до 4 294 967 295
короткая 2 байта От -32 768 до 32 767
неподписанный короткий 2 байта От 0 до 65 535
долго 4 байта От -2 147 483 648 до 2 147 483 647
без знака долго 4 байта От 0 до 4 294 967 295

Чтобы получить точный размер типа или переменной на конкретной платформе, вы можете использовать оператор sizeof . Выражение sizeof (тип) возвращает размер хранилища объекта или типа в байтах. Ниже приведен пример получения размера типа int на любой машине:

Live Demo

#include <stdio.h>
#include <limits.h>

int main() {
   printf("Storage size for int : %d n", sizeof(int));
   
   return 0;
}

Когда вы компилируете и запускаете вышеупомянутую программу, она дает следующий результат в Linux:

Storage size for int : 4

Типы с плавающей точкой

В следующей таблице приведены сведения о стандартных типах с плавающей точкой с размерами хранения и диапазонами значений, а также их точностью.

Тип Размер хранилища Диапазон значений точность
поплавок 4 байта 1,2E-38 до 3,4E + 38 6 десятичных знаков
двойной 8 байт 2,3E-308 до 1,7E + 308 15 десятичных знаков
длинный двойной 10 байт От 3.4E-4932 до 1.1E + 4932 19 десятичных знаков

Заголовочный файл float.h определяет макросы, которые позволяют вам использовать эти значения и другие подробности о двоичном представлении действительных чисел в ваших программах. В следующем примере печатается место для хранения, занятое типом с плавающей запятой, и его значениями диапазона.

Live Demo

#include <stdio.h>
#include <float.h>

int main() {
   printf("Storage size for float : %d n", sizeof(float));
   printf("Minimum float positive value: %En", FLT_MIN );
   printf("Maximum float positive value: %En", FLT_MAX );
   printf("Precision value: %dn", FLT_DIG );
   
   return 0;
}

Когда вы компилируете и запускаете вышеупомянутую программу, она дает следующий результат в Linux:

Storage size for float : 4
Minimum float positive value: 1.175494E-38
Maximum float positive value: 3.402823E+38
Precision value: 6

Тип пустоты

Тип void указывает, что значение недоступно. Он используется в трех видах ситуаций –

Sr.No. Типы и описание
1

Функция возвращается как void

В C есть различные функции, которые не возвращают никакого значения, или вы можете сказать, что они возвращают void. Функция без возвращаемого значения имеет тип возврата как void. Например, void exit (int status);

2

Аргументы функции как void

В C есть различные функции, которые не принимают никаких параметров. Функция без параметра может принять пустоту. Например, int rand (void);

3

Указатели на аннулирование

Указатель типа void * представляет адрес объекта, но не его тип. Например, функция выделения памяти void * malloc (size_t size); возвращает указатель на void, который может быть приведен к любому типу данных.

Функция возвращается как void

В C есть различные функции, которые не возвращают никакого значения, или вы можете сказать, что они возвращают void. Функция без возвращаемого значения имеет тип возврата как void. Например, void exit (int status);

Аргументы функции как void

В C есть различные функции, которые не принимают никаких параметров. Функция без параметра может принять пустоту. Например, int rand (void);

Указатели на аннулирование

Указатель типа void * представляет адрес объекта, но не его тип. Например, функция выделения памяти void * malloc (size_t size); возвращает указатель на void, который может быть приведен к любому типу данных.

C – переменные

Переменная – это не что иное, как имя, данное области памяти, которой могут манипулировать наши программы. Каждая переменная в C имеет определенный тип, который определяет размер и расположение памяти переменной; диапазон значений, которые могут быть сохранены в этой памяти; и набор операций, которые могут быть применены к переменной.

Имя переменной может состоять из букв, цифр и символа подчеркивания. Он должен начинаться либо с буквы, либо с подчеркивания. Прописные и строчные буквы различны, потому что C чувствителен к регистру. Основываясь на базовых типах, описанных в предыдущей главе, будут следующие базовые типы переменных:

Sr.No. Тип и описание
1

голец

Обычно один октет (один байт). Это целочисленный тип.

2

ИНТ

Наиболее натуральный размер целого числа для машины.

3

поплавок

Значение с плавающей запятой одинарной точности.

4

двойной

Значение с плавающей запятой двойной точности.

5

недействительным

Представляет отсутствие типа.

голец

Обычно один октет (один байт). Это целочисленный тип.

ИНТ

Наиболее натуральный размер целого числа для машины.

поплавок

Значение с плавающей запятой одинарной точности.

двойной

Значение с плавающей запятой двойной точности.

недействительным

Представляет отсутствие типа.

Язык программирования C также позволяет определять различные другие типы переменных, которые мы рассмотрим в следующих главах, таких как Перечисление, Указатель, Массив, Структура, Объединение и т. Д. В этой главе мы изучим только основные типы переменных.

Определение переменной в C

Определение переменной сообщает компилятору, где и сколько памяти нужно создать для переменной. Определение переменной определяет тип данных и содержит список из одной или нескольких переменных этого типа следующим образом:

type variable_list;

Здесь тип должен быть допустимым типом данных C, включая char, w_char, int, float, double, bool или любой определенный пользователем объект; и variable_list может состоять из одного или нескольких имен идентификаторов, разделенных запятыми. Некоторые действительные объявления показаны здесь –

int    i, j, k;
char   c, ch;
float  f, salary;
double d;

Линия int i, j, k; объявляет и определяет переменные i, j и k; который инструктирует компилятор создавать переменные с именами i, j и k типа int.

Переменные могут быть инициализированы (им присвоено начальное значение) в их объявлении. Инициализатор состоит из знака равенства, за которым следует постоянное выражение:

type variable_name = value;

Вот некоторые примеры:

extern int d = 3, f = 5;    // declaration of d and f. 
int d = 3, f = 5;           // definition and initializing d and f. 
byte z = 22;                // definition and initializes z. 
char x = 'x';               // the variable x has the value 'x'.

Для определения без инициализатора: переменные со статической продолжительностью хранения неявно инициализируются с помощью NULL (все байты имеют значение 0); начальное значение всех остальных переменных не определено.

Объявление переменных в C

Объявление переменной дает гарантию компилятору, что существует переменная с заданным типом и именем, так что компилятор может продолжить компиляцию, не требуя полной информации о переменной. Определение переменной имеет смысл только во время компиляции, компилятору требуется фактическое определение переменной во время компоновки программы.

Объявление переменной полезно, когда вы используете несколько файлов и определяете свою переменную в одном из файлов, которые будут доступны во время компоновки программы. Вы будете использовать ключевое слово extern для объявления переменной в любом месте. Хотя вы можете объявлять переменную несколько раз в вашей C-программе, она может быть определена только один раз в файле, функции или блоке кода.

пример

Попробуйте следующий пример, где переменные были объявлены сверху, но они были определены и инициализированы внутри основной функции –

Live Demo

#include <stdio.h>

// Variable declaration:
extern int a, b;
extern int c;
extern float f;

int main () {

   /* variable definition: */
   int a, b;
   int c;
   float f;
 
   /* actual initialization */
   a = 10;
   b = 20;
  
   c = a + b;
   printf("value of c : %d n", c);

   f = 70.0/3.0;
   printf("value of f : %f n", f);
 
   return 0;
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

value of c : 30
value of f : 23.333334

Та же концепция применяется к объявлению функции, когда вы предоставляете имя функции во время ее объявления, и ее фактическое определение может быть дано где-либо еще. Например –

// function declaration
int func();

int main() {

   // function call
   int i = func();
}

// function definition
int func() {
   return 0;
}

L-значения и R-значения в C

Есть два вида выражений в C –

  • lvalue – выражения, которые ссылаются на ячейку памяти, называются «lvalue» выражениями. Lvalue может отображаться как левая или правая сторона задания.

  • rvalue – термин rvalue относится к значению данных, которое хранится по некоторому адресу в памяти. Значение r – это выражение, которому не может быть присвоено значение, что означает, что значение r может появляться в правой части, но не в левой части назначения.

lvalue – выражения, которые ссылаются на ячейку памяти, называются «lvalue» выражениями. Lvalue может отображаться как левая или правая сторона задания.

rvalue – термин rvalue относится к значению данных, которое хранится по некоторому адресу в памяти. Значение r – это выражение, которому не может быть присвоено значение, что означает, что значение r может появляться в правой части, но не в левой части назначения.

Переменные являются l-значениями, поэтому они могут отображаться в левой части назначения. Числовые литералы являются r-значениями, поэтому они не могут быть назначены и не могут отображаться слева. Взгляните на следующие действительные и недействительные утверждения –

int g = 20; // valid statement

10 = 20; // invalid statement; would generate compile-time error

C – константы и литералы

Константы относятся к фиксированным значениям, которые программа не может изменить во время своего выполнения. Эти фиксированные значения также называются литералами .

Константы могут быть любого из основных типов данных, таких как целочисленная константа, плавающая константа, символьная константа или строковый литерал . Есть и константы перечисления.

Константы обрабатываются как обычные переменные, за исключением того, что их значения не могут быть изменены после их определения.

Целочисленные литералы

Целочисленный литерал может быть десятичной, восьмеричной или шестнадцатеричной константой. Префикс указывает основание или основание: 0x или 0X для шестнадцатеричного, 0 для восьмеричного и ничего для десятичного.

Целочисленный литерал также может иметь суффикс, который представляет собой комбинацию U и L для беззнакового и длинного соответственно. Суффикс может быть в верхнем или нижнем регистре и может быть в любом порядке.

Вот несколько примеров целочисленных литералов –

212         /* Legal */
215u        /* Legal */
0xFeeL      /* Legal */
078         /* Illegal: 8 is not an octal digit */
032UU       /* Illegal: cannot repeat a suffix */

Ниже приведены другие примеры различных типов целочисленных литералов –

85         /* decimal */
0213       /* octal */
0x4b       /* hexadecimal */
30         /* int */
30u        /* unsigned int */
30l        /* long */
30ul       /* unsigned long */

Литералы с плавающей точкой

Литерал с плавающей точкой имеет целочисленную часть, десятичную точку, дробную часть и экспоненту. Вы можете представлять литералы с плавающей запятой в десятичной или экспоненциальной форме.

Представляя десятичную форму, вы должны включить десятичную точку, показатель степени или оба; и при представлении экспоненциальной формы вы должны включать целую часть, дробную часть или оба. Подписанный показатель вводится через e или E.

Вот несколько примеров литералов с плавающей точкой –

3.14159       /* Legal */
314159E-5L    /* Legal */
510E          /* Illegal: incomplete exponent */
210f          /* Illegal: no decimal or exponent */
.e55          /* Illegal: missing integer or fraction */

Константы персонажа

Символьные литералы заключены в одинарные кавычки, например, ‘x’ может храниться в простой переменной типа char .

Символьный литерал может быть простым символом (например, «x»), escape-последовательностью (например, « t») или универсальным символом (например, « u02C0»).

В С есть определенные символы, представляющие особое значение, когда им предшествует обратная косая черта, например, новая строка ( n) или табуляция ( t).

Здесь у вас есть список таких кодов escape-последовательностей –

Последовательность побега Имея в виду
\ персонаж
» ‘ персонаж
» ” персонаж
? ? персонаж
а Оповещение или звонок
б возврат на одну позицию
е Форма подачи
п Новая линия
р Возврат каретки
т Горизонтальная вкладка
v Вертикальная вкладка
ооо Восьмеричное число от одной до трех цифр
ххх , , Шестнадцатеричное число из одной или нескольких цифр

Ниже приведен пример, показывающий несколько символов escape-последовательности:

Live Demo

#include <stdio.h>

int main() {
   printf("HellotWorldnn");

   return 0;
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

Hello World

Строковые литералы

Строковые литералы или константы заключаются в двойные кавычки “”. Строка содержит символы, похожие на символьные литералы: простые символы, escape-последовательности и универсальные символы.

Вы можете разбить длинную строку на несколько строк, используя строковые литералы и разделяя их пробелами.

Вот несколько примеров строковых литералов. Все три формы являются одинаковыми строками.

"hello, dear"

"hello, 

dear"

"hello, " "d" "ear"

Определение констант

Есть два простых способа определения констант в C:

  • Использование #define препроцессора.

  • Используя ключевое слово const .

Использование #define препроцессора.

Используя ключевое слово const .

Препроцессор #define

Ниже приведена форма для использования препроцессора #define для определения константы –

#define identifier value

Следующий пример объясняет это подробно –

Live Demo

#include <stdio.h>

#define LENGTH 10   
#define WIDTH  5
#define NEWLINE 'n'

int main() {
   int area;  
  
   area = LENGTH * WIDTH;
   printf("value of area : %d", area);
   printf("%c", NEWLINE);

   return 0;
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

value of area : 50

Ключевое слово const

Вы можете использовать префикс const для объявления констант определенного типа следующим образом:

const type variable = value;

Следующий пример объясняет это подробно –

Live Demo

#include <stdio.h>

int main() {
   const int  LENGTH = 10;
   const int  WIDTH = 5;
   const char NEWLINE = 'n';
   int area;  
   
   area = LENGTH * WIDTH;
   printf("value of area : %d", area);
   printf("%c", NEWLINE);

   return 0;
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

value of area : 50

Обратите внимание, что это хорошая практика программирования для определения констант в заглавных буквах.

C – Классы хранения

Класс хранения определяет область действия (видимость) и время жизни переменных и / или функций в программе на Си. Они предшествуют типу, который они изменяют. У нас есть четыре разных класса хранения в программе на C –

  • авто
  • регистр
  • статический
  • внешний

Авто Класс Хранения

Класс автоматического хранения является классом хранения по умолчанию для всех локальных переменных.

{
   int mount;
   auto int month;
}

В приведенном выше примере определены две переменные в одном классе хранения. ‘auto’ может использоваться только внутри функций, то есть локальных переменных.

Класс хранения регистра

Класс хранения регистров используется для определения локальных переменных, которые должны храниться в регистре, а не в ОЗУ. Это означает, что переменная имеет максимальный размер, равный размеру регистра (обычно одно слово), и к ней не может быть применен унарный оператор ‘&’ (так как она не имеет места в памяти).

{
   register int  miles;
}

Регистр следует использовать только для переменных, которые требуют быстрого доступа, таких как счетчики. Следует также отметить, что определение «регистр» не означает, что переменная будет храниться в регистре. Это означает, что он МОЖЕТ храниться в реестре в зависимости от аппаратного обеспечения и ограничений реализации.

Статический класс хранения

Класс статического хранилища инструктирует компилятор сохранять локальную переменную в течение всей жизни программы, а не создавать и уничтожать ее каждый раз, когда она входит и выходит из области видимости. Следовательно, статические локальные переменные позволяют им сохранять свои значения между вызовами функций.

Статический модификатор также может применяться к глобальным переменным. Когда это сделано, область действия этой переменной будет ограничена файлом, в котором она объявлена.

В программировании на C, когда static используется в глобальной переменной, он вызывает совместное использование только одной копии этого члена всеми объектами этого класса.

Live Demo

#include <stdio.h>
 
/* function declaration */
void func(void);
 
static int count = 5; /* global variable */
 
main() {

   while(count--) {
      func();
   }
	
   return 0;
}

/* function definition */
void func( void ) {

   static int i = 5; /* local static variable */
   i++;

   printf("i is %d and count is %dn", i, count);
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

i is 6 and count is 4
i is 7 and count is 3
i is 8 and count is 2
i is 9 and count is 1
i is 10 and count is 0

Внешний класс хранения

Класс внешнего хранилища используется для предоставления ссылки на глобальную переменную, которая видна ВСЕМ программным файлам. Когда вы используете ‘extern’, переменная не может быть инициализирована, однако, она указывает имя переменной на место хранения, которое было ранее определено.

Если у вас есть несколько файлов, и вы определяете глобальную переменную или функцию, которые также будут использоваться в других файлах, тогда extern будет использоваться в другом файле для предоставления ссылки на определенную переменную или функцию. Просто для понимания, extern используется для объявления глобальной переменной или функции в другом файле.

Модификатор extern чаще всего используется, когда два или более файлов совместно используют одни и те же глобальные переменные или функции, как описано ниже.

Первый файл: main.c

#include <stdio.h>
 
int count ;
extern void write_extern();
 
main() {
   count = 5;
   write_extern();
}

Второй файл: support.c

#include <stdio.h>
 
extern int count;
 
void write_extern(void) {
   printf("count is %dn", count);
}

Здесь extern используется для объявления счетчика во втором файле, где, как и его определение в первом файле, main.c. Теперь скомпилируйте эти два файла следующим образом:

$gcc main.c support.c

Это произведет исполняемую программу a.out . Когда эта программа выполняется, она дает следующий результат –

count is 5

C – Операторы

Оператор – это символ, который указывает компилятору выполнять определенные математические или логические функции. Язык C богат встроенными операторами и предоставляет следующие типы операторов:

  • Арифметические Операторы
  • Операторы отношений
  • Логические Операторы
  • Битовые операторы
  • Операторы присваивания
  • Разные Операторы

В этой главе мы рассмотрим, как работает каждый оператор.

Арифметические Операторы

В следующей таблице показаны все арифметические операторы, поддерживаемые языком Си. Предположим, что переменная A содержит 10, а переменная B содержит 20, тогда –

Показать примеры

оператор Описание пример
+ Добавляет два операнда. А + В = 30
Вычитает второй операнд из первого. A – B = -10
* Умножает оба операнда. A * B = 200
/ Делит числитель на числитель. B / A = 2
% Оператор модуля и остаток от целочисленного деления. B% A = 0
++ Оператор приращения увеличивает целочисленное значение на единицу. А ++ = 11
Оператор уменьшения уменьшает целочисленное значение на единицу. A– = 9

Операторы отношений

В следующей таблице показаны все реляционные операторы, поддерживаемые C. Предположим, переменная A содержит 10, а переменная B содержит 20, тогда –

Показать примеры

оператор Описание пример
== Проверяет, равны ли значения двух операндов или нет. Если да, то условие становится истинным. (A == B) не соответствует действительности.
знак равно Проверяет, равны ли значения двух операндов или нет. Если значения не равны, то условие становится истинным. (A! = B) верно.
> Проверяет, больше ли значение левого операнда, чем значение правого операнда. Если да, то условие становится истинным. (A> B) не соответствует действительности.
< Проверяет, меньше ли значение левого операнда, чем значение правого операнда. Если да, то условие становится истинным. (A <B) верно.
> = Проверяет, больше ли значение левого операнда или равно значению правого операнда. Если да, то условие становится истинным. (A> = B) не соответствует действительности.
<= Проверяет, меньше ли значение левого операнда или равно значению правого операнда. Если да, то условие становится истинным. (A <= B) верно.

Логические Операторы

В следующей таблице приведены все логические операторы, поддерживаемые языком Си. Предположим, что переменная A содержит 1, а переменная B содержит 0, тогда –

Показать примеры

оператор Описание пример
&& Называется логический оператор И. Если оба операнда отличны от нуля, условие становится истинным. (A && B) неверно.
|| Вызывается логическим оператором ИЛИ. Если любой из двух операндов отличен от нуля, условие становится истинным. (A || B) верно.
! Вызывается логическим оператором НЕ. Он используется для изменения логического состояния своего операнда. Если условие истинно, то оператор Логический НЕ сделает его ложным. ! (A && B) верно.

Битовые операторы

Побитовый оператор работает с битами и выполняет побитовую операцию. Таблицы истинности для &, | и ^ следующие:

п Q P & Q р | Q р ^ д
0 0 0 0 0
0 1 0 1 1
1 1 1 1 0
1 0 0 1 1

Предположим, что A = 60 и B = 13 в двоичном формате, они будут выглядеть следующим образом:

A = 0011 1100

B = 0000 1101

—————–

A & B = 0000 1100

A | B = 0011 1101

A ^ B = 0011 0001

~ A = 1100 0011

В следующей таблице перечислены побитовые операторы, поддерживаемые C. Предположим, переменная «A» содержит 60, а переменная «B» содержит 13, затем –

Показать примеры

оператор Описание пример
& Двоичный оператор AND немного копирует результат, если он существует в обоих операндах. (A & B) = 12, т.е. 0000 1100
| Оператор двоичного ИЛИ копирует немного, если он существует в любом из операндов. (A | B) = 61, т. Е. 0011 1101
^ Двоичный оператор XOR копирует бит, если он установлен в одном операнде, но не в обоих. (A ^ B) = 49, т.е. 0011 0001
~ Оператор дополнения Binary One является унарным и имеет эффект «переворачивания» битов. (~ A) = ~ (60), т. Е. -0111101
<< Двоичный оператор левого сдвига. Значение левого операнда перемещается влево на количество битов, указанное правым операндом. A << 2 = 240, т. Е. 1111 0000
>> Оператор двоичного правого сдвига. Значение левого операнда перемещается вправо на количество битов, указанное правым операндом. A >> 2 = 15, т.е. 0000 1111

Операторы присваивания

В следующей таблице перечислены операторы присваивания, поддерживаемые языком C –

Показать примеры

оператор Описание пример
знак равно Простой оператор присваивания. Назначает значения от правых операндов к левому операнду C = A + B назначит значение A + B для C
+ = Добавить И оператор присваивания. Он добавляет правый операнд к левому операнду и присваивает результат левому операнду. C + = A эквивалентно C = C + A
знак равно Вычитание И оператор присваивания. Он вычитает правый операнд из левого операнда и присваивает результат левому операнду. C – = A эквивалентно C = C – A
знак равно Оператор умножения И присваивания. Он умножает правый операнд на левый операнд и присваивает результат левому операнду. C * = A эквивалентно C = C * A
знак равно Оператор деления И присваивания. Он делит левый операнд на правый операнд и присваивает результат левому операнду. C / = A эквивалентно C = C / A
знак равно Модуль И оператор присваивания. Он принимает модуль с использованием двух операндов и присваивает результат левому операнду. C% = A эквивалентно C = C% A
<< = Левый сдвиг И оператор присваивания. C << = 2 совпадает с C = C << 2
>> = Сдвиг вправо И оператор присваивания. C >> = 2 – это то же самое, что C = C >> 2
знак равно Побитовое И оператор присваивания. C & = 2 совпадает с C = C & 2
^ = Побитовое исключающее ИЛИ и оператор присваивания. C ^ = 2 совпадает с C = C ^ 2
| = Побитовое ИЛИ и оператор присваивания. C | = 2 – это то же самое, что C = C | 2

Разные операторы ↦ sizeof & ternary

Помимо операторов, описанных выше, есть несколько других важных операторов, включая sizeof и ? : поддерживается языком Си.

Показать примеры

оператор Описание пример
размер() Возвращает размер переменной. sizeof (a), где a является целым числом, вернет 4.
& Возвращает адрес переменной. & А; возвращает фактический адрес переменной.
* Указатель на переменную. * А;
? : Условное выражение. Если условие верно? тогда значение X: иначе значение Y

Приоритет операторов в C

Приоритет оператора определяет группировку терминов в выражении и определяет способ вычисления выражения. Некоторые операторы имеют более высокий приоритет, чем другие; например, оператор умножения имеет более высокий приоритет, чем оператор сложения.

Например, х = 7 + 3 * 2; здесь x назначено 13, а не 20, потому что оператор * имеет более высокий приоритет, чем +, поэтому он сначала умножается на 3 * 2, а затем прибавляется к 7.

Здесь операторы с самым высоким приоритетом отображаются вверху таблицы, а операторы с самым низким – внизу. Внутри выражения операторы с более высоким приоритетом будут оцениваться первыми.

Показать примеры

категория оператор Ассоциативность
постфикс () [] ->. ++ – – Слева направо
Одинарный + -! ~ ++ – – (тип) * & sizeof Справа налево
Multiplicative * /% Слева направо
присадка + – Слева направо
сдвиг << >> Слева направо
реляционный <<=>> = Слева направо
равенство ==! = Слева направо
Побитовое И & Слева направо
Побитовый XOR ^ Слева направо
Побитовое ИЛИ | Слева направо
Логическое И && Слева направо
Логическое ИЛИ || Слева направо
условный ?: Справа налево
присваивание = + = – = * = / =% = >> = << = & = ^ = | = Справа налево
запятая , Слева направо

C – Принятие решений

Структуры принятия решений требуют, чтобы программист определял одно или несколько условий, которые должны быть оценены или протестированы программой, вместе с оператором или инструкциями, которые должны быть выполнены, если условие определено как истинное, и, необязательно, другие операторы, которые должны быть выполнены, если условие определяется как ложный.

Ниже приведена общая форма типичной структуры принятия решений, встречающейся в большинстве языков программирования.

Принятие решений заявления в C

Язык программирования C принимает любые ненулевые и ненулевые значения как истинные , и если это ноль или ноль , то это предполагается как ложное значение.

Язык программирования C предоставляет следующие типы операторов принятия решений.

Sr.No. Заявление и описание
1 если заявление

Оператор if состоит из логического выражения, за которым следует одно или несколько операторов.

2 если … еще заявление

За оператором if может следовать необязательный оператор else , который выполняется, когда логическое выражение имеет значение false.

3 вложенные операторы if

Вы можете использовать один оператор if или else if внутри другого оператора if или else if .

4 заявление о переключении

Оператор switch позволяет проверять переменную на соответствие списку значений.

5 вложенные операторы switch

Вы можете использовать один оператор switch внутри другого оператора (ов) switch .

Оператор if состоит из логического выражения, за которым следует одно или несколько операторов.

За оператором if может следовать необязательный оператор else , который выполняется, когда логическое выражение имеет значение false.

Вы можете использовать один оператор if или else if внутри другого оператора if или else if .

Оператор switch позволяет проверять переменную на соответствие списку значений.

Вы можете использовать один оператор switch внутри другого оператора (ов) switch .

? : Оператор

Мы накрыли условного оператора? : в предыдущей главе, которая может быть использована для замены операторов if … else . Он имеет следующую общую форму –

Exp1 ? Exp2 : Exp3;

Где Exp1, Exp2 и Exp3 являются выражениями. Обратите внимание на использование и размещение толстой кишки.

Значение? выражение определяется так –

  • Exp1 оценивается. Если это правда, то Exp2 оценивается и становится значением целого? выражение.

  • Если Exp1 имеет значение false, то Exp3 оценивается, и его значение становится значением выражения.

Exp1 оценивается. Если это правда, то Exp2 оценивается и становится значением целого? выражение.

Если Exp1 имеет значение false, то Exp3 оценивается, и его значение становится значением выражения.

C – Петли

Вы можете столкнуться с ситуациями, когда блок кода должен выполняться несколько раз. В общем случае операторы выполняются последовательно: первый оператор в функции выполняется первым, затем второй и так далее.

Языки программирования предоставляют различные управляющие структуры, которые допускают более сложные пути выполнения.

Оператор цикла позволяет нам выполнять оператор или группу операторов несколько раз. Ниже приведена общая форма оператора цикла в большинстве языков программирования –

Петлевая архитектура

Язык программирования C предоставляет следующие типы циклов для обработки требований циклов.

Sr.No. Тип и описание петли
1 в то время как цикл

Повторяет оператор или группу операторов, пока данное условие выполняется. Он проверяет условие перед выполнением тела цикла.

2 для цикла

Выполняет последовательность операторов несколько раз и сокращает код, который управляет переменной цикла.

3 делать … пока цикл

Это больше похоже на оператор while, за исключением того, что он проверяет условие в конце тела цикла.

4 вложенные циклы

Вы можете использовать один или несколько циклов внутри любого другого цикла while, for или do.. while.

Повторяет оператор или группу операторов, пока данное условие выполняется. Он проверяет условие перед выполнением тела цикла.

Выполняет последовательность операторов несколько раз и сокращает код, который управляет переменной цикла.

Это больше похоже на оператор while, за исключением того, что он проверяет условие в конце тела цикла.

Вы можете использовать один или несколько циклов внутри любого другого цикла while, for или do.. while.

Заявления о контроле цикла

Операторы управления циклом изменяют выполнение от его нормальной последовательности. Когда выполнение покидает область действия, все автоматические объекты, созданные в этой области, уничтожаются.

C поддерживает следующие операторы управления.

Sr.No. Контрольное заявление и описание
1 заявление о нарушении

Завершает оператор цикла или переключателя и передает выполнение в оператор, следующий сразу за циклом или переключателем.

2 продолжить заявление

Заставляет петлю пропускать оставшуюся часть своего тела и немедленно проверять свое состояние перед повторением.

3 Перейти к заявлению

Передает управление помеченному выражению.

Завершает оператор цикла или переключателя и передает выполнение в оператор, следующий сразу за циклом или переключателем.

Заставляет петлю пропускать оставшуюся часть своего тела и немедленно проверять свое состояние перед повторением.

Передает управление помеченному выражению.

Бесконечный цикл

Цикл становится бесконечным, если условие никогда не становится ложным. Цикл for традиционно используется для этой цели. Поскольку ни одно из трех выражений, образующих цикл for, не требуется, вы можете создать бесконечный цикл, оставив условное выражение пустым.

#include <stdio.h>
 
int main () {

   for( ; ; ) {
      printf("This loop will run forever.n");
   }

   return 0;
}

Когда условное выражение отсутствует, оно считается истинным. У вас может быть выражение инициализации и приращения, но программисты на Си чаще используют конструкцию for (;;) для обозначения бесконечного цикла.

ПРИМЕЧАНИЕ. – Вы можете завершить бесконечный цикл, нажав клавиши Ctrl + C.

C – Функции

Функция – это группа операторов, которые вместе выполняют задачу. Каждая программа на С имеет как минимум одну функцию – main () , и все самые тривиальные программы могут определять дополнительные функции.

Вы можете разделить ваш код на отдельные функции. Как вы делите свой код между различными функциями, зависит от вас, но логически разделение таково, что каждая функция выполняет определенную задачу.

Объявление функции сообщает компилятору об имени функции, типе возврата и параметрах. Определение функции обеспечивает фактическое тело функции.

Стандартная библиотека C предоставляет множество встроенных функций, которые может вызывать ваша программа. Например, strcat () для объединения двух строк, memcpy () для копирования одной области памяти в другую и многие другие функции.

Функцию также можно назвать методом, подпрограммой, процедурой и т. Д.

Определение функции

Общая форма определения функции в языке программирования C выглядит следующим образом:

return_type function_name( parameter list ) {
   body of the function
}

Определение функции в C-программировании состоит из заголовка функции и тела функции . Вот все части функции –

  • Тип возврата – функция может возвращать значение. Return_type – это тип данных значения, которое возвращает функция. Некоторые функции выполняют нужные операции без возврата значения. В этом случае return_type является ключевым словом void .

  • Имя функции – это фактическое имя функции. Имя функции и список параметров вместе составляют сигнатуру функции.

  • Параметры – параметр похож на заполнитель. Когда вызывается функция, вы передаете значение параметру. Это значение называется фактическим параметром или аргументом. Список параметров относится к типу, порядку и количеству параметров функции. Параметры являются необязательными; то есть функция может не содержать параметров.

  • Тело функцииТело функции содержит набор операторов, которые определяют, что делает функция.

Тип возврата – функция может возвращать значение. Return_type – это тип данных значения, которое возвращает функция. Некоторые функции выполняют нужные операции без возврата значения. В этом случае return_type является ключевым словом void .

Имя функции – это фактическое имя функции. Имя функции и список параметров вместе составляют сигнатуру функции.

Параметры – параметр похож на заполнитель. Когда вызывается функция, вы передаете значение параметру. Это значение называется фактическим параметром или аргументом. Список параметров относится к типу, порядку и количеству параметров функции. Параметры являются необязательными; то есть функция может не содержать параметров.

Тело функцииТело функции содержит набор операторов, которые определяют, что делает функция.

пример

Ниже приведен исходный код функции max () . Эта функция принимает два параметра num1 и num2 и возвращает максимальное значение между двумя –

/* function returning the max between two numbers */
int max(int num1, int num2) {

   /* local variable declaration */
   int result;
 
   if (num1 > num2)
      result = num1;
   else
      result = num2;
 
   return result; 
}

Объявления функций

Объявление функции сообщает компилятору об имени функции и о том, как вызывать функцию. Фактическое тело функции может быть определено отдельно.

Объявление функции состоит из следующих частей:

return_type function_name( parameter list );

Для определенной выше функции max () объявление функции выглядит следующим образом:

int max(int num1, int num2);

Имена параметров не важны в объявлении функции, требуется только их тип, поэтому следующее также является допустимым объявлением:

int max(int, int);

Объявление функции требуется, когда вы определяете функцию в одном исходном файле и вызываете эту функцию в другом файле. В таком случае вы должны объявить функцию в начале файла, вызывающего функцию.

Вызов функции

При создании функции C вы даете определение того, что должна делать функция. Чтобы использовать функцию, вам нужно будет вызвать эту функцию для выполнения определенной задачи.

Когда программа вызывает функцию, управление программой передается вызываемой функции. Вызываемая функция выполняет определенную задачу, и когда выполняется ее оператор return или когда достигается закрывающая фигурная скобка, завершающая функцию, она возвращает управление программой обратно в основную программу.

Чтобы вызвать функцию, вам просто нужно передать необходимые параметры вместе с именем функции, и если функция возвращает значение, вы можете сохранить возвращенное значение. Например –

Live Demo

#include <stdio.h>
 
/* function declaration */
int max(int num1, int num2);
 
int main () {

   /* local variable definition */
   int a = 100;
   int b = 200;
   int ret;
 
   /* calling a function to get max value */
   ret = max(a, b);
 
   printf( "Max value is : %dn", ret );
 
   return 0;
}
 
/* function returning the max between two numbers */
int max(int num1, int num2) {

   /* local variable declaration */
   int result;
 
   if (num1 > num2)
      result = num1;
   else
      result = num2;
 
   return result; 
}

Мы сохранили max () вместе с main () и скомпилировали исходный код. Запустив финальный исполняемый файл, он даст следующий результат:

Max value is : 200

Аргументы функции

Если функция должна использовать аргументы, она должна объявлять переменные, которые принимают значения аргументов. Эти переменные называются формальными параметрами функции.

Формальные параметры ведут себя как другие локальные переменные внутри функции и создаются при входе в функцию и уничтожаются при выходе.

При вызове функции есть два способа передачи аргументов в функцию:

Sr.No. Тип звонка и описание
1 Звонок по значению

Этот метод копирует фактическое значение аргумента в формальный параметр функции. В этом случае изменения, внесенные в параметр внутри функции, не влияют на аргумент.

2 Звоните по ссылке

Этот метод копирует адрес аргумента в формальный параметр. Внутри функции адрес используется для доступа к фактическому аргументу, используемому в вызове. Это означает, что изменения, внесенные в параметр, влияют на аргумент.

Этот метод копирует фактическое значение аргумента в формальный параметр функции. В этом случае изменения, внесенные в параметр внутри функции, не влияют на аргумент.

Этот метод копирует адрес аргумента в формальный параметр. Внутри функции адрес используется для доступа к фактическому аргументу, используемому в вызове. Это означает, что изменения, внесенные в параметр, влияют на аргумент.

По умолчанию C использует вызов по значению для передачи аргументов. В общем, это означает, что код внутри функции не может изменить аргументы, используемые для вызова функции.

C – Правила области

Область действия в любом программировании – это область программы, в которой может существовать определенная переменная, и за пределами этой переменной к ней нельзя получить доступ. Есть три места, где переменные могут быть объявлены на языке программирования C –

  • Внутри функции или блока, который называется локальными переменными.

  • Снаружи всех функций, которые называются глобальными переменными.

  • В определении параметров функции, которые называются формальными параметрами.

Внутри функции или блока, который называется локальными переменными.

Снаружи всех функций, которые называются глобальными переменными.

В определении параметров функции, которые называются формальными параметрами.

Давайте разберемся, что такое локальные и глобальные переменные, а также формальные параметры.

Локальные переменные

Переменные, которые объявлены внутри функции или блока, называются локальными переменными. Они могут использоваться только операторами, которые находятся внутри этой функции или блока кода. Локальные переменные не известны функциям вне их собственных. В следующем примере показано, как используются локальные переменные. Здесь все переменные a, b и c являются локальными для функции main ().

Live Demo

 #include <stdio.h>
 
 int main () {

   / * объявление локальной переменной * /
   int a, b;
   int c;
 
   / * фактическая инициализация * /
   а = 10;
   б = 20;
   с = а + б;
 
   printf («значение a =% d, b =% d и c =% d  n», a, b, c);
 
   вернуть 0;
 }

Глобальные переменные

Глобальные переменные определяются вне функции, обычно в верхней части программы. Глобальные переменные хранят свои значения в течение всего жизненного цикла вашей программы, и к ним можно получить доступ в любой из функций, определенных для программы.

Глобальная переменная может быть доступна любой функции. То есть глобальная переменная доступна для использования во всей вашей программе после ее объявления. Следующая программа показывает, как глобальные переменные используются в программе.

Live Demo

#include <stdio.h>
 
/* global variable declaration */
int g;
 
int main () {

  /* local variable declaration */
  int a, b;
 
  /* actual initialization */
  a = 10;
  b = 20;
  g = a + b;
 
  printf ("value of a = %d, b = %d and g = %dn", a, b, g);
 
  return 0;
}

Программа может иметь одинаковые имена для локальных и глобальных переменных, но предпочтение будет отдаваться значению локальной переменной внутри функции. Вот пример –

Live Demo

#include <stdio.h>
 
/* global variable declaration */
int g = 20;
 
int main () {

  /* local variable declaration */
  int g = 10;
 
  printf ("value of g = %dn",  g);
 
  return 0;
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

value of g = 10

Формальные параметры

Формальные параметры обрабатываются как локальные переменные внутри функции и имеют приоритет над глобальными переменными. Ниже приведен пример –

Live Demo

#include <stdio.h>
 
/* global variable declaration */
int a = 20;
 
int main () {

  /* local variable declaration in main function */
  int a = 10;
  int b = 20;
  int c = 0;

  printf ("value of a in main() = %dn",  a);
  c = sum( a, b);
  printf ("value of c in main() = %dn",  c);

  return 0;
}

/* function to add two integers */
int sum(int a, int b) {

   printf ("value of a in sum() = %dn",  a);
   printf ("value of b in sum() = %dn",  b);

   return a + b;
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

value of a in main() = 10
value of a in sum() = 10
value of b in sum() = 20
value of c in main() = 30

Инициализация локальных и глобальных переменных

Когда определена локальная переменная, она не инициализируется системой, вы должны инициализировать ее самостоятельно. Глобальные переменные автоматически инициализируются системой, когда вы определяете их следующим образом:

Тип данных Начальное значение по умолчанию
ИНТ 0
голец ‘ 0’
поплавок 0
двойной 0
указатель НОЛЬ

Хорошей практикой программирования является правильная инициализация переменных, в противном случае ваша программа может дать неожиданные результаты, потому что неинициализированные переменные будут принимать некоторое значение мусора, уже имеющееся в их ячейке памяти.

C – Массивы

Массивы – это структура данных, в которой можно хранить последовательную коллекцию элементов одного и того же типа. Массив используется для хранения коллекции данных, но часто более полезно думать о массиве как о коллекции переменных одного типа.

Вместо того, чтобы объявлять отдельные переменные, такие как number0, number1, … и number99, вы объявляете одну переменную массива, такую ​​как числа, и используете числа [0], числа [1] и …, числа [99] для представления отдельные переменные. Определенный элемент в массиве доступен по индексу.

Все массивы состоят из смежных областей памяти. Самый низкий адрес соответствует первому элементу, а самый высокий адрес – последнему.

Массивы в Си

Объявление массивов

Чтобы объявить массив в C, программист определяет тип элементов и количество элементов, требуемых массивом, следующим образом:

type arrayName [ arraySize ];

Это называется одномерным массивом. ArraySize должен быть целочисленной константой, большей нуля, и тип может быть любым допустимым типом данных C. Например, чтобы объявить массив из 10 элементов с именем balance типа double, используйте этот оператор –

double balance[10];

Здесь баланс – это переменная, которой достаточно для хранения до 10 двойных чисел.

Инициализация массивов

Вы можете инициализировать массив в C один за другим или использовать один оператор следующим образом:

double balance[5] = {1000.0, 2.0, 3.4, 7.0, 50.0};

Число значений в фигурных скобках {} не может быть больше, чем количество элементов, которые мы объявляем для массива в квадратных скобках [].

Если вы опустите размер массива, будет создан массив, достаточно большой, чтобы вместить инициализацию. Поэтому, если вы напишите –

double balance[] = {1000.0, 2.0, 3.4, 7.0, 50.0};

Вы создадите точно такой же массив, как и в предыдущем примере. Ниже приведен пример назначения одного элемента массива:

balance[4] = 50.0;

Приведенный выше оператор присваивает 5- й элемент в массиве со значением 50,0. Все массивы имеют 0 в качестве индекса своего первого элемента, который также называется базовым индексом, а последний индекс массива будет иметь общий размер массива минус 1. Ниже показано графическое представление массива, который мы обсуждали выше –

Презентация массива

Доступ к элементам массива

Доступ к элементу осуществляется путем индексации имени массива. Это делается путем помещения индекса элемента в квадратные скобки после имени массива. Например –

double salary = balance[9];

Приведенный выше оператор возьмет 10- й элемент из массива и присвоит значение переменной salary. В следующем примере показано, как использовать все три вышеупомянутых понятия, а именно. объявление, присваивание и доступ к массивам –

Live Demo

#include <stdio.h>
 
int main () {

   int n[ 10 ]; /* n is an array of 10 integers */
   int i,j;
 
   /* initialize elements of array n to 0 */         
   for ( i = 0; i < 10; i++ ) {
      n[ i ] = i + 100; /* set element at location i to i + 100 */
   }
   
   /* output each array element's value */
   for (j = 0; j < 10; j++ ) {
      printf("Element[%d] = %dn", j, n[j] );
   }
 
   return 0;
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

Element[0] = 100
Element[1] = 101
Element[2] = 102
Element[3] = 103
Element[4] = 104
Element[5] = 105
Element[6] = 106
Element[7] = 107
Element[8] = 108
Element[9] = 109

Массивы в деталях

Массивы важны для C и должны уделять гораздо больше внимания. Следующие важные понятия, связанные с массивом, должны быть понятны программисту C –

Sr.No. Концепция и описание
1 Многомерные массивы

C поддерживает многомерные массивы. Простейшей формой многомерного массива является двумерный массив.

2 Передача массивов в функции

Вы можете передать функции указатель на массив, указав имя массива без индекса.

3 Возврат массива из функции

C позволяет функции возвращать массив.

4 Указатель на массив

Вы можете сгенерировать указатель на первый элемент массива, просто указав имя массива без индекса.

C поддерживает многомерные массивы. Простейшей формой многомерного массива является двумерный массив.

Вы можете передать функции указатель на массив, указав имя массива без индекса.

C позволяет функции возвращать массив.

Вы можете сгенерировать указатель на первый элемент массива, просто указав имя массива без индекса.

C – указатели

Указатели на C легки и интересны для изучения. Некоторые задачи программирования на Си легче выполняются с помощью указателей, а другие задачи, такие как динамическое распределение памяти, не могут быть выполнены без использования указателей. Поэтому становится необходимым изучать указатели, чтобы стать идеальным программистом Си. Давайте начнем изучать их в простых и легких шагах.

Как вы знаете, каждая переменная является ячейкой памяти, и каждая ячейка памяти имеет свой адрес, к которому можно обратиться, используя оператор амперсанда (&), который обозначает адрес в памяти. Рассмотрим следующий пример, который печатает адрес определенных переменных:

Live Demo

#include <stdio.h>

int main () {

   int  var1;
   char var2[10];

   printf("Address of var1 variable: %xn", &var1  );
   printf("Address of var2 variable: %xn", &var2  );

   return 0;
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

Address of var1 variable: bff5a400
Address of var2 variable: bff5a3f6

Что такое указатели?

Указатель – это переменная, значением которой является адрес другой переменной, т. Е. Прямой адрес ячейки памяти. Как и любая переменная или константа, вы должны объявить указатель, прежде чем использовать его для хранения любого адреса переменной. Общая форма объявления переменной указателя –

type *var-name;

Здесь тип – это базовый тип указателя; это должен быть допустимый тип данных C, а var-name – это имя переменной-указателя. Звездочка *, используемая для объявления указателя, такая же, как звездочка, используемая для умножения. Однако в этом утверждении звездочка используется для обозначения переменной в качестве указателя. Взгляните на некоторые из допустимых объявлений указателей –

int    *ip;    /* pointer to an integer */
double *dp;    /* pointer to a double */
float  *fp;    /* pointer to a float */
char   *ch     /* pointer to a character */

Фактический тип данных значения всех указателей, будь то целое число, число с плавающей запятой, символ или другое, является одним и тем же, длинное шестнадцатеричное число, представляющее адрес памяти. Единственное различие между указателями разных типов данных – это тип данных переменной или константы, на которую указывает указатель.

Как использовать указатели?

Есть несколько важных операций, которые мы будем делать с помощью указателей очень часто. (а) мы определяем переменную указателя, (б) назначаем адрес переменной указателю и (в) наконец получаем доступ к значению по адресу, доступному в переменной указателя. Это делается с помощью унарного оператора *, который возвращает значение переменной, расположенной по адресу, указанному ее операндом. В следующем примере используются эти операции –

Live Demo

#include <stdio.h>

int main () {

   int  var = 20;   /* actual variable declaration */
   int  *ip;        /* pointer variable declaration */

   ip = &var;  /* store address of var in pointer variable*/

   printf("Address of var variable: %xn", &var  );

   /* address stored in pointer variable */
   printf("Address stored in ip variable: %xn", ip );

   /* access the value using the pointer */
   printf("Value of *ip variable: %dn", *ip );

   return 0;
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

Address of var variable: bffd8b3c
Address stored in ip variable: bffd8b3c
Value of *ip variable: 20

NULL указатели

Хорошей практикой всегда является присвоение значения NULL переменной-указателю, если у вас нет точного адреса для назначения. Это делается во время объявления переменной. Указатель, которому присвоен NULL, называется нулевым указателем.

Указатель NULL – это константа со значением ноль, определенная в нескольких стандартных библиотеках. Рассмотрим следующую программу –

Live Demo

#include <stdio.h>

int main () {

   int  *ptr = NULL;

   printf("The value of ptr is : %xn", ptr  );
 
   return 0;
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

The value of ptr is 0

В большинстве операционных систем программам не разрешен доступ к памяти по адресу 0, поскольку эта память зарезервирована операционной системой. Однако адрес памяти 0 имеет особое значение; он сигнализирует о том, что указатель не предназначен для указания на доступную ячейку памяти. Но по соглашению, если указатель содержит нулевое (нулевое) значение, предполагается, что он ничего не указывает.

Чтобы проверить нулевой указатель, вы можете использовать оператор if следующим образом:

if(ptr)     /* succeeds if p is not null */
if(!ptr)    /* succeeds if p is null */

Указатели в деталях

У указателей есть много, но простых концепций, и они очень важны для программирования на Си. Следующие важные концепции указателей должны быть понятны любому программисту C –

Sr.No. Концепция и описание
1 Арифметика указателей

В указателях можно использовать четыре арифметических оператора: ++, -, +, –

2 Массив указателей

Вы можете определить массивы для хранения нескольких указателей.

3 Указатель на указатель

C позволяет вам иметь указатель на указатель и так далее.

4 Передача указателей на функции в C

Передача аргумента по ссылке или по адресу позволяет вызывающему функции изменять переданный аргумент в вызывающей функции.

5 Возврат указателя из функций в C

C позволяет функции возвращать указатель на локальную переменную, статическую переменную и динамически выделенную память.

В указателях можно использовать четыре арифметических оператора: ++, -, +, –

Вы можете определить массивы для хранения нескольких указателей.

C позволяет вам иметь указатель на указатель и так далее.

Передача аргумента по ссылке или по адресу позволяет вызывающему функции изменять переданный аргумент в вызывающей функции.

C позволяет функции возвращать указатель на локальную переменную, статическую переменную и динамически выделенную память.

C – Струны

Строки на самом деле являются одномерным массивом символов, оканчивающихся нулевым символом ‘ 0’. Таким образом, строка с нулевым символом в конце содержит символы, которые составляют строку, за которой следует ноль .

Следующее объявление и инициализация создают строку, состоящую из слова «Hello». Чтобы держать нулевой символ в конце массива, размер массива символов, содержащего строку, на один больше, чем количество символов в слове «Hello».

char greeting[6] = {'H', 'e', 'l', 'l', 'o', ''};

Если вы следуете правилу инициализации массива, вы можете написать приведенное выше утверждение следующим образом:

char greeting[] = "Hello";

Ниже приводится представление в памяти определенной выше строки в C / C ++ –

Строковая презентация в C / C ++

На самом деле, вы не помещаете нулевой символ в конец строковой константы. Компилятор C автоматически помещает ‘ 0’ в конец строки, когда инициализирует массив. Давайте попробуем напечатать вышеупомянутую строку –

Live Demo

#include <stdio.h>

int main () {

   char greeting[6] = {'H', 'e', 'l', 'l', 'o', ''};
   printf("Greeting message: %sn", greeting );
   return 0;
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

Greeting message: Hello

C поддерживает широкий спектр функций, которые обрабатывают строки с нулевым символом в конце –

Sr.No. Функция и цель
1

strcpy (s1, s2);

Копирует строку s2 в строку s1.

2

strcat (s1, s2);

Объединяет строку s2 в конец строки s1.

3

StrLen (S1);

Возвращает длину строки s1.

4

strcmp (s1, s2);

Возвращает 0, если s1 и s2 одинаковы; меньше 0, если s1 <s2; больше 0, если s1> s2.

5

strchr (s1, ch);

Возвращает указатель на первое вхождение символа ch в строке s1.

6

strstr (s1, s2);

Возвращает указатель на первое вхождение строки s2 в строку s1.

strcpy (s1, s2);

Копирует строку s2 в строку s1.

strcat (s1, s2);

Объединяет строку s2 в конец строки s1.

StrLen (S1);

Возвращает длину строки s1.

strcmp (s1, s2);

Возвращает 0, если s1 и s2 одинаковы; меньше 0, если s1 <s2; больше 0, если s1> s2.

strchr (s1, ch);

Возвращает указатель на первое вхождение символа ch в строке s1.

strstr (s1, s2);

Возвращает указатель на первое вхождение строки s2 в строку s1.

В следующем примере используются некоторые из вышеупомянутых функций –

Live Demo

#include <stdio.h>
#include <string.h>

int main () {

   char str1[12] = "Hello";
   char str2[12] = "World";
   char str3[12];
   int  len ;

   /* copy str1 into str3 */
   strcpy(str3, str1);
   printf("strcpy( str3, str1) :  %sn", str3 );

   /* concatenates str1 and str2 */
   strcat( str1, str2);
   printf("strcat( str1, str2):   %sn", str1 );

   /* total lenghth of str1 after concatenation */
   len = strlen(str1);
   printf("strlen(str1) :  %dn", len );

   return 0;
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

strcpy( str3, str1) :  Hello
strcat( str1, str2):   HelloWorld
strlen(str1) :  10

C – Структуры

Массивы позволяют определить тип переменных, которые могут содержать несколько элементов данных одного вида. Аналогичным образом структура – это еще один определяемый пользователем тип данных, доступный в C, который позволяет объединять элементы данных разных типов.

Структуры используются для представления записи. Предположим, вы хотите отслеживать свои книги в библиотеке. Вы можете отслеживать следующие атрибуты о каждой книге –

  • заглавие
  • автор
  • Предмет
  • ID книги

Определение структуры

Чтобы определить структуру, вы должны использовать инструкцию struct . Оператор struct определяет новый тип данных с более чем одним членом. Формат оператора структуры следующий:

struct [structure tag] {

   member definition;
   member definition;
   ...
   member definition;
} [one or more structure variables];  

Тег структуры является необязательным, и каждое определение члена является обычным определением переменной, например int i; или плавать f; или любое другое допустимое определение переменной. В конце определения структуры перед последней точкой с запятой вы можете указать одну или несколько переменных структуры, но это не обязательно. Вот как вы бы объявили структуру Книги –

struct Books {
   char  title[50];
   char  author[50];
   char  subject[100];
   int   book_id;
} book;  

Доступ к членам структуры

Чтобы получить доступ к любому члену структуры, мы используем оператор доступа к члену (.) . Оператор доступа к элементу кодируется как точка между именем структурной переменной и элементом структуры, к которому мы хотим получить доступ. Вы бы использовали ключевое слово struct для определения переменных типа структуры. В следующем примере показано, как использовать структуру в программе.

Live Demo

#include <stdio.h>
#include <string.h>
 
struct Books {
   char  title[50];
   char  author[50];
   char  subject[100];
   int   book_id;
};
 
int main( ) {

   struct Books Book1;        /* Declare Book1 of type Book */
   struct Books Book2;        /* Declare Book2 of type Book */
 
   /* book 1 specification */
   strcpy( Book1.title, "C Programming");
   strcpy( Book1.author, "Nuha Ali"); 
   strcpy( Book1.subject, "C Programming Tutorial");
   Book1.book_id = 6495407;

   /* book 2 specification */
   strcpy( Book2.title, "Telecom Billing");
   strcpy( Book2.author, "Zara Ali");
   strcpy( Book2.subject, "Telecom Billing Tutorial");
   Book2.book_id = 6495700;
 
   /* print Book1 info */
   printf( "Book 1 title : %sn", Book1.title);
   printf( "Book 1 author : %sn", Book1.author);
   printf( "Book 1 subject : %sn", Book1.subject);
   printf( "Book 1 book_id : %dn", Book1.book_id);

   /* print Book2 info */
   printf( "Book 2 title : %sn", Book2.title);
   printf( "Book 2 author : %sn", Book2.author);
   printf( "Book 2 subject : %sn", Book2.subject);
   printf( "Book 2 book_id : %dn", Book2.book_id);

   return 0;
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

Book 1 title : C Programming
Book 1 author : Nuha Ali
Book 1 subject : C Programming Tutorial
Book 1 book_id : 6495407
Book 2 title : Telecom Billing
Book 2 author : Zara Ali
Book 2 subject : Telecom Billing Tutorial
Book 2 book_id : 6495700

Структуры как аргументы функций

Вы можете передать структуру в качестве аргумента функции так же, как передаете любую другую переменную или указатель.

Live Demo

#include <stdio.h>
#include <string.h>
 
struct Books {
   char  title[50];
   char  author[50];
   char  subject[100];
   int   book_id;
};

/* function declaration */
void printBook( struct Books book );

int main( ) {

   struct Books Book1;        /* Declare Book1 of type Book */
   struct Books Book2;        /* Declare Book2 of type Book */
 
   /* book 1 specification */
   strcpy( Book1.title, "C Programming");
   strcpy( Book1.author, "Nuha Ali"); 
   strcpy( Book1.subject, "C Programming Tutorial");
   Book1.book_id = 6495407;

   /* book 2 specification */
   strcpy( Book2.title, "Telecom Billing");
   strcpy( Book2.author, "Zara Ali");
   strcpy( Book2.subject, "Telecom Billing Tutorial");
   Book2.book_id = 6495700;
 
   /* print Book1 info */
   printBook( Book1 );

   /* Print Book2 info */
   printBook( Book2 );

   return 0;
}

void printBook( struct Books book ) {

   printf( "Book title : %sn", book.title);
   printf( "Book author : %sn", book.author);
   printf( "Book subject : %sn", book.subject);
   printf( "Book book_id : %dn", book.book_id);
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

Book title : C Programming
Book author : Nuha Ali
Book subject : C Programming Tutorial
Book book_id : 6495407
Book title : Telecom Billing
Book author : Zara Ali
Book subject : Telecom Billing Tutorial
Book book_id : 6495700

Указатели на структуры

Вы можете определять указатели на структуры так же, как вы определяете указатель на любую другую переменную –

struct Books *struct_pointer;

Теперь вы можете сохранить адрес структурной переменной в указанной выше переменной-указателе. Чтобы найти адрес структурной переменной, поместите ‘&’; оператор перед именем структуры следующим образом –

struct_pointer = &Book1;

Чтобы получить доступ к членам структуры, используя указатель на эту структуру, вы должны использовать оператор → следующим образом:

struct_pointer->title;

Давайте перепишем приведенный выше пример, используя указатель структуры.

Live Demo

#include <stdio.h>
#include <string.h>
 
struct Books {
   char  title[50];
   char  author[50];
   char  subject[100];
   int   book_id;
};

/* function declaration */
void printBook( struct Books *book );
int main( ) {

   struct Books Book1;        /* Declare Book1 of type Book */
   struct Books Book2;        /* Declare Book2 of type Book */
 
   /* book 1 specification */
   strcpy( Book1.title, "C Programming");
   strcpy( Book1.author, "Nuha Ali"); 
   strcpy( Book1.subject, "C Programming Tutorial");
   Book1.book_id = 6495407;

   /* book 2 specification */
   strcpy( Book2.title, "Telecom Billing");
   strcpy( Book2.author, "Zara Ali");
   strcpy( Book2.subject, "Telecom Billing Tutorial");
   Book2.book_id = 6495700;
 
   /* print Book1 info by passing address of Book1 */
   printBook( &Book1 );

   /* print Book2 info by passing address of Book2 */
   printBook( &Book2 );

   return 0;
}

void printBook( struct Books *book ) {

   printf( "Book title : %sn", book->title);
   printf( "Book author : %sn", book->author);
   printf( "Book subject : %sn", book->subject);
   printf( "Book book_id : %dn", book->book_id);
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

Book title : C Programming
Book author : Nuha Ali
Book subject : C Programming Tutorial
Book book_id : 6495407
Book title : Telecom Billing
Book author : Zara Ali
Book subject : Telecom Billing Tutorial
Book book_id : 6495700

Битовые поля

Битовые поля позволяют упаковывать данные в структуру. Это особенно полезно, когда память или хранилище данных стоят дорого. Типичные примеры включают –

  • Упаковка нескольких объектов в машинное слово. например, 1-битные флаги могут быть сжаты.

  • Чтение внешних форматов файлов – могут быть прочитаны нестандартные форматы файлов, например, 9-разрядные целые числа.

Упаковка нескольких объектов в машинное слово. например, 1-битные флаги могут быть сжаты.

Чтение внешних форматов файлов – могут быть прочитаны нестандартные форматы файлов, например, 9-разрядные целые числа.

C позволяет нам сделать это в определении структуры, поместив: bit length после переменной. Например –

struct packed_struct {
   unsigned int f1:1;
   unsigned int f2:1;
   unsigned int f3:1;
   unsigned int f4:1;
   unsigned int type:4;
   unsigned int my_int:9;
} pack;

Здесь pack_struct содержит 6 членов: четыре 1-битных флага f1..f3, 4-битный тип и 9-битный my_int.

C автоматически упаковывает вышеуказанные битовые поля настолько компактно, насколько это возможно, при условии, что максимальная длина поля меньше или равна целочисленной длине слова компьютера. Если это не так, то некоторые компиляторы могут допускать перекрытие памяти для полей, в то время как другие сохраняют следующее поле в следующем слове.

C – Союзы

Объединение – это специальный тип данных, доступный в C, который позволяет хранить разные типы данных в одной и той же ячейке памяти. Вы можете определить объединение со многими членами, но только один член может содержать значение в любой момент времени. Союзы предоставляют эффективный способ использования одной и той же области памяти для многоцелевого использования.

Определение союза

Чтобы определить объединение, вы должны использовать оператор объединения так же, как при определении структуры. Оператор union определяет новый тип данных с более чем одним членом для вашей программы. Формат заявления объединения следующий:

union [union tag] {
   member definition;
   member definition;
   ...
   member definition;
} [one or more union variables];  

Тег объединения является необязательным, и каждое определение члена является обычным определением переменной, например int i; или плавать f; или любое другое допустимое определение переменной. В конце определения объединения перед последней точкой с запятой вы можете указать одну или несколько переменных объединения, но это необязательно. Вот как вы можете определить тип объединения с именем Data, имеющий три члена i, f и str.

union Data {
   int i;
   float f;
   char str[20];
} data;  

Теперь переменная типа Data может хранить целое число, число с плавающей точкой или строку символов. Это означает, что одна переменная, т. Е. Одна и та же ячейка памяти, может использоваться для хранения нескольких типов данных. Вы можете использовать любые встроенные или определенные пользователем типы данных внутри объединения в зависимости от ваших требований.

Память, занятая профсоюзом, будет достаточно большой, чтобы вместить самого крупного члена профсоюза. Например, в приведенном выше примере тип данных будет занимать 20 байт пространства памяти, поскольку это максимальное пространство, которое может занимать строка символов. В следующем примере показан общий объем памяти, занимаемый вышеуказанным объединением –

Live Demo

#include <stdio.h>
#include <string.h>
 
union Data {
   int i;
   float f;
   char str[20];
};
 
int main( ) {

   union Data data;        

   printf( "Memory size occupied by data : %dn", sizeof(data));

   return 0;
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

Memory size occupied by data : 20

Доступ к членам Союза

Чтобы получить доступ к любому члену профсоюза, мы используем оператор доступа члена (.) . Оператор доступа к члену кодируется как точка между именем переменной объединения и членом объединения, к которому мы хотим получить доступ. Вы бы использовали ключевое слово union для определения переменных типа union. В следующем примере показано, как использовать объединения в программе –

Live Demo

#include <stdio.h>
#include <string.h>
 
union Data {
   int i;
   float f;
   char str[20];
};
 
int main( ) {

   union Data data;        

   data.i = 10;
   data.f = 220.5;
   strcpy( data.str, "C Programming");

   printf( "data.i : %dn", data.i);
   printf( "data.f : %fn", data.f);
   printf( "data.str : %sn", data.str);

   return 0;
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

data.i : 1917853763
data.f : 4122360580327794860452759994368.000000
data.str : C Programming

Здесь мы можем видеть, что значения членов i и f объединения были повреждены, потому что последнее значение, присвоенное переменной, заняло место в памяти, и это является причиной того, что значение члена str печатается очень хорошо.

Теперь давайте снова посмотрим на тот же пример, где мы будем использовать одну переменную за раз, что является основной целью создания объединений –

Live Demo

#include <stdio.h>
#include <string.h>
 
union Data {
   int i;
   float f;
   char str[20];
};
 
int main( ) {

   union Data data;        

   data.i = 10;
   printf( "data.i : %dn", data.i);
   
   data.f = 220.5;
   printf( "data.f : %fn", data.f);
   
   strcpy( data.str, "C Programming");
   printf( "data.str : %sn", data.str);

   return 0;
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

data.i : 10
data.f : 220.500000
data.str : C Programming

Здесь все участники печатаются очень хорошо, потому что один член используется за один раз.

C – битовые поля

Предположим, что ваша C-программа содержит несколько переменных TRUE / FALSE, сгруппированных в структуру, называемую status, следующим образом:

struct {
   unsigned int widthValidated;
   unsigned int heightValidated;
} status;

Эта структура требует 8 байт памяти, но в действительности мы собираемся хранить 0 или 1 в каждой из переменных. Язык программирования C предлагает лучший способ использовать пространство памяти в таких ситуациях.

Если вы используете такие переменные внутри структуры, вы можете определить ширину переменной, которая сообщает компилятору C, что вы собираетесь использовать только это количество байтов. Например, приведенная выше структура может быть переписана следующим образом:

struct {
   unsigned int widthValidated : 1;
   unsigned int heightValidated : 1;
} status;

Приведенная выше структура требует 4 байта пространства памяти для переменной состояния, но только 2 бита будут использоваться для хранения значений.

Если вы будете использовать до 32 переменных, каждая из которых имеет ширину 1 бит, то в структуре состояния также будет использоваться 4 байта. Однако, как только у вас будет 33 переменные, он выделит следующий слот памяти и начнет использовать 8 байтов. Давайте проверим следующий пример, чтобы понять концепцию –

Live Demo

#include <stdio.h>
#include <string.h>

/* define simple structure */
struct {
   unsigned int widthValidated;
   unsigned int heightValidated;
} status1;

/* define a structure with bit fields */
struct {
   unsigned int widthValidated : 1;
   unsigned int heightValidated : 1;
} status2;
 
int main( ) {
   printf( "Memory size occupied by status1 : %dn", sizeof(status1));
   printf( "Memory size occupied by status2 : %dn", sizeof(status2));
   return 0;
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

Memory size occupied by status1 : 8
Memory size occupied by status2 : 4

Объявление битового поля

Объявление битового поля имеет следующую форму внутри структуры:

struct {
   type [member_name] : width ;
};

В следующей таблице описаны переменные элементы битового поля –

Sr.No. Элемент и описание
1

тип

Целочисленный тип, который определяет, как интерпретируется значение битового поля. Тип может быть int, подписанный int или беззнаковый int.

2

имя участника

Название битового поля.

3

ширина

Количество битов в битовом поле. Ширина должна быть меньше или равна битовой ширине указанного типа.

тип

Целочисленный тип, который определяет, как интерпретируется значение битового поля. Тип может быть int, подписанный int или беззнаковый int.

имя участника

Название битового поля.

ширина

Количество битов в битовом поле. Ширина должна быть меньше или равна битовой ширине указанного типа.

Переменные, определенные с предопределенной шириной, называются битовыми полями . Битовое поле может содержать более одного бита; например, если вам нужна переменная для хранения значения от 0 до 7, вы можете определить битовое поле шириной 3 бита следующим образом:

struct {
   unsigned int age : 3;
} Age;

Приведенное выше определение структуры указывает компилятору C, что переменная age будет использовать только 3 бита для хранения значения. Если вы попытаетесь использовать более 3 бит, то это не позволит вам сделать это. Давайте попробуем следующий пример –

Live Demo

#include <stdio.h>
#include <string.h>

struct {
   unsigned int age : 3;
} Age;

int main( ) {

   Age.age = 4;
   printf( "Sizeof( Age ) : %dn", sizeof(Age) );
   printf( "Age.age : %dn", Age.age );

   Age.age = 7;
   printf( "Age.age : %dn", Age.age );

   Age.age = 8;
   printf( "Age.age : %dn", Age.age );

   return 0;
}

Когда приведенный выше код компилируется, он компилируется с предупреждением, а при выполнении выдает следующий результат:

Sizeof( Age ) : 4
Age.age : 4
Age.age : 7
Age.age : 0

C – typedef

Язык программирования C предоставляет ключевое слово typedef , которое можно использовать для присвоения типу нового имени. Ниже приведен пример определения термина BYTE для однобайтовых чисел:

typedef unsigned char BYTE;

После этого определения типа идентификатор BYTE может использоваться, например, как сокращение для типа unsigned char. ,

BYTE  b1, b2;

По соглашению, в этих определениях используются заглавные буквы, чтобы напомнить пользователю, что имя типа действительно является символическим сокращением, но вы можете использовать строчные буквы следующим образом:

typedef unsigned char byte;

Вы также можете использовать typedef, чтобы дать имя вашим пользовательским типам данных. Например, вы можете использовать typedef со структурой для определения нового типа данных, а затем использовать этот тип данных для непосредственного определения структурных переменных следующим образом:

Live Demo

#include <stdio.h>
#include <string.h>
 
typedef struct Books {
   char title[50];
   char author[50];
   char subject[100];
   int book_id;
} Book;
 
int main( ) {

   Book book;
 
   strcpy( book.title, "C Programming");
   strcpy( book.author, "Nuha Ali"); 
   strcpy( book.subject, "C Programming Tutorial");
   book.book_id = 6495407;
 
   printf( "Book title : %sn", book.title);
   printf( "Book author : %sn", book.author);
   printf( "Book subject : %sn", book.subject);
   printf( "Book book_id : %dn", book.book_id);

   return 0;
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

Book  title : C Programming
Book  author : Nuha Ali
Book  subject : C Programming Tutorial
Book  book_id : 6495407

typedef против #define

#define – это C-директива, которая также используется для определения псевдонимов для различных типов данных, аналогичных typedef, но со следующими отличиями:

  • typedef ограничен предоставлением символических имен только для типов, где как #define может использоваться для определения псевдонима для значений, q. вы можете определить 1 как ONE и т. д.

  • Интерпретация typedef выполняется компилятором, тогда как операторы #define обрабатываются препроцессором.

typedef ограничен предоставлением символических имен только для типов, где как #define может использоваться для определения псевдонима для значений, q. вы можете определить 1 как ONE и т. д.

Интерпретация typedef выполняется компилятором, тогда как операторы #define обрабатываются препроцессором.

В следующем примере показано, как использовать #define в программе –

Live Demo

#include <stdio.h>
 
#define TRUE  1
#define FALSE 0
 
int main( ) {
   printf( "Value of TRUE : %dn", TRUE);
   printf( "Value of FALSE : %dn", FALSE);

   return 0;
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

Value of TRUE : 1
Value of FALSE : 0

C – вход и выход

Когда мы говорим « Ввод» , это означает ввод некоторых данных в программу. Входные данные могут быть предоставлены в форме файла или из командной строки. Программирование на С предоставляет набор встроенных функций для считывания заданного ввода и подачи его в программу согласно требованию.

Когда мы говорим « Вывод» , это означает отображение некоторых данных на экране, принтере или в любом файле. С-программирование предоставляет набор встроенных функций для вывода данных на экран компьютера, а также для сохранения их в текстовых или двоичных файлах.

Стандартные файлы

C программирование рассматривает все устройства как файлы. Таким образом, такие устройства, как дисплей, адресуются так же, как и файлы, и следующие три файла автоматически открываются при запуске программы для обеспечения доступа к клавиатуре и экрану.

Стандартный файл Файловый указатель устройство
Стандартный ввод STDIN клавиатура
Стандартный вывод стандартный вывод экран
Стандартная ошибка STDERR Ваш экран

Файловые указатели являются средством доступа к файлу для чтения и записи. В этом разделе объясняется, как считывать значения с экрана и как печатать результат на экране.

Функции getchar () и putchar ()

Функция int getchar (void) считывает следующий доступный символ с экрана и возвращает его как целое число. Эта функция читает только один символ за раз. Вы можете использовать этот метод в цикле, если вы хотите прочитать более одного символа с экрана.

Функция int putchar (int c) помещает переданный символ на экран и возвращает тот же символ. Эта функция помещает только один символ за раз. Вы можете использовать этот метод в цикле, если вы хотите отобразить более одного символа на экране. Проверьте следующий пример –

#include <stdio.h>
int main( ) {

   int c;

   printf( "Enter a value :");
   c = getchar( );

   printf( "nYou entered: ");
   putchar( c );

   return 0;
}

Когда приведенный выше код скомпилирован и выполнен, он ждет, когда вы введете какой-то текст. Когда вы вводите текст и нажимаете ввод, программа продолжает работу и читает только один символ и отображает его следующим образом:

$./a.out
Enter a value : this is test
You entered: t

Функции gets () и put ()

Функция char * gets (char * s) считывает строку из stdin в буфер, на который указывает s, до завершающей строки или EOF (End of File).

Функция int put (const char * s) записывает завершающие символы новой строки ‘s’ и ‘a’ в стандартный вывод .

#include <stdio.h>
int main( ) {

   char str[100];

   printf( "Enter a value :");
   gets( str );

   printf( "nYou entered: ");
   puts( str );

   return 0;
}

Когда приведенный выше код скомпилирован и выполнен, он ждет, когда вы введете какой-то текст. Когда вы вводите текст и нажимаете ввод, программа продолжает и читает всю строку до конца и отображает ее следующим образом:

$./a.out
Enter a value : this is test
You entered: this is test

Функции scanf () и printf ()

Функция int scanf (const char * format, …) считывает ввод из стандартного потока ввода stdin и сканирует этот ввод в соответствии с предоставленным форматом .

Функция int printf (const char * format, …) записывает вывод в стандартный поток вывода stdout и производит вывод в соответствии с предоставленным форматом.

Формат может быть простой константной строкой, но вы можете указать% s,% d,% c,% f и т. Д. Для печати или чтения строк, целых чисел, символов или с плавающей точкой соответственно. Есть много других доступных вариантов форматирования, которые можно использовать в зависимости от требований. Давайте теперь перейдем к простому примеру, чтобы лучше понять концепции –

#include <stdio.h>
int main( ) {

   char str[100];
   int i;

   printf( "Enter a value :");
   scanf("%s %d", str, &i);

   printf( "nYou entered: %s %d ", str, i);

   return 0;
}

Когда приведенный выше код скомпилирован и выполнен, он ждет, когда вы введете какой-то текст. Когда вы вводите текст и нажимаете ввод, программа переходит и читает ввод и отображает его следующим образом:

$./a.out
Enter a value : seven 7
You entered: seven 7

Здесь следует отметить, что scanf () ожидает ввод в том же формате, что вы указали% s и% d, что означает, что вы должны предоставить допустимые вводы, такие как «строковое целое число». Если вы укажете «string string» или «integer integer», то это будет считаться неправильным вводом. Во-вторых, при чтении строки scanf () прекращает чтение, как только встречает пробел, поэтому «это тест» – три строки для scanf ().

C – File I / O

В последней главе описаны стандартные устройства ввода и вывода, которые обрабатываются языком программирования C. В этой главе рассказывается, как программисты на C могут создавать, открывать, закрывать текстовые или двоичные файлы для хранения своих данных.

Файл представляет собой последовательность байтов, независимо от того, является ли он текстовым файлом или двоичным файлом. Язык программирования C обеспечивает доступ к функциям высокого уровня, а также к вызовам низкого уровня (уровня ОС) для обработки файлов на ваших устройствах хранения. Эта глава проведет вас через важные призывы к управлению файлами.

Открытие файлов

Вы можете использовать функцию fopen (), чтобы создать новый файл или открыть существующий файл. Этот вызов инициализирует объект типа FILE , который содержит всю информацию, необходимую для управления потоком. Прототип этого вызова функции выглядит следующим образом:

FILE *fopen( const char * filename, const char * mode );

Здесь filename является строковым литералом, который вы будете использовать для именования вашего файла, а режим доступа может иметь одно из следующих значений:

Sr.No. Режим и описание
1

р

Открывает существующий текстовый файл для чтения.

2

вес

Открывает текстовый файл для записи. Если он не существует, то создается новый файл. Здесь ваша программа начнет запись содержимого с начала файла.

3

Открывает текстовый файл для записи в режиме добавления. Если он не существует, то создается новый файл. Здесь ваша программа начнет добавлять содержимое в существующий файл содержимого.

4

г +

Открывает текстовый файл для чтения и записи.

5

ш +

Открывает текстовый файл для чтения и записи. Сначала он обрезает файл до нулевой длины, если он существует, в противном случае создает файл, если он не существует.

6

а +

Открывает текстовый файл для чтения и записи. Он создает файл, если он не существует. Чтение начнется с самого начала, но запись может быть только добавлена.

р

Открывает существующий текстовый файл для чтения.

вес

Открывает текстовый файл для записи. Если он не существует, то создается новый файл. Здесь ваша программа начнет запись содержимого с начала файла.

Открывает текстовый файл для записи в режиме добавления. Если он не существует, то создается новый файл. Здесь ваша программа начнет добавлять содержимое в существующий файл содержимого.

г +

Открывает текстовый файл для чтения и записи.

ш +

Открывает текстовый файл для чтения и записи. Сначала он обрезает файл до нулевой длины, если он существует, в противном случае создает файл, если он не существует.

а +

Открывает текстовый файл для чтения и записи. Он создает файл, если он не существует. Чтение начнется с самого начала, но запись может быть только добавлена.

Если вы собираетесь обрабатывать двоичные файлы, вы будете использовать следующие режимы доступа вместо вышеупомянутых:

"rb", "wb", "ab", "rb+", "r+b", "wb+", "w+b", "ab+", "a+b"

Закрытие файла

Чтобы закрыть файл, используйте функцию fclose (). Прототип этой функции –

int fclose( FILE *fp );

Функция fclose (-) возвращает ноль в случае успеха или EOF, если при закрытии файла произошла ошибка. Эта функция фактически сбрасывает любые данные, все еще ожидающие в буфере, в файл, закрывает файл и освобождает любую память, используемую для файла. EOF является константой, определенной в заголовочном файле stdio.h .

В стандартной библиотеке C есть различные функции для чтения и записи файла, символ за символом или в форме строки фиксированной длины.

Написание файла

Ниже приводится простейшая функция для записи отдельных символов в поток.

int fputc( int c, FILE *fp );

Функция fputc () записывает символьное значение аргумента c в выходной поток, на который ссылается fp. Он возвращает письменный символ, написанный при успехе, в противном случае EOF, если есть ошибка. Вы можете использовать следующие функции для записи строки с нулевым символом в конце –

int fputs( const char *s, FILE *fp );

Функция fputs () записывает строку s в выходной поток, на который ссылается fp. Он возвращает неотрицательное значение в случае успеха, в противном случае EOF возвращается в случае любой ошибки. Вы также можете использовать функцию int fprintf (FILE * fp, const char * format, …) для записи строки в файл. Попробуйте следующий пример.

Убедитесь, что у вас есть каталог / tmp . Если это не так, то прежде чем продолжить, вы должны создать этот каталог на вашем компьютере.

#include <stdio.h>

main() {
   FILE *fp;

   fp = fopen("/tmp/test.txt", "w+");
   fprintf(fp, "This is testing for fprintf...n");
   fputs("This is testing for fputs...n", fp);
   fclose(fp);
}

Когда приведенный выше код компилируется и выполняется, он создает новый файл test.txt в каталоге / tmp и записывает две строки, используя две разные функции. Давайте прочитаем этот файл в следующем разделе.

Чтение файла

Ниже приведена простейшая функция для чтения одного символа из файла –

int fgetc( FILE * fp );

Функция fgetc () читает символ из входного файла, на который ссылается fp. Возвращаемым значением является прочитанный символ, или в случае любой ошибки он возвращает EOF . Следующая функция позволяет читать строку из потока –

char *fgets( char *buf, int n, FILE *fp );

Функция fgets () читает до n-1 символов из входного потока, на который ссылается fp. Он копирует прочитанную строку в буфер buf , добавляя нулевой символ для завершения строки.

Если эта функция встречает символ новой строки ‘ n’ или конец файла EOF до того, как они прочитают максимальное количество символов, то она возвращает только символы, считанные до этой точки, включая символ новой строки. Вы также можете использовать функцию int fscanf (FILE * fp, const char * format, …) для чтения строк из файла, но она прекращает чтение после обнаружения первого пробела.

#include <stdio.h>

main() {

   FILE *fp;
   char buff[255];

   fp = fopen("/tmp/test.txt", "r");
   fscanf(fp, "%s", buff);
   printf("1 : %sn", buff );

   fgets(buff, 255, (FILE*)fp);
   printf("2: %sn", buff );
   
   fgets(buff, 255, (FILE*)fp);
   printf("3: %sn", buff );
   fclose(fp);

}

Когда приведенный выше код компилируется и выполняется, он читает файл, созданный в предыдущем разделе, и выдает следующий результат:

1 : This
2: is testing for fprintf...

3: This is testing for fputs...

Давайте посмотрим немного подробнее о том, что здесь произошло. Во-первых, fscanf () читает именно это, потому что после этого он обнаружил пробел, второй вызов для fgets (), который читает оставшуюся строку, пока не встретит конец строки. Наконец, последний вызов fgets () полностью читает вторую строку.

Двоичные функции ввода / вывода

Есть две функции, которые можно использовать для двоичного ввода и вывода –

size_t fread(void *ptr, size_t size_of_elements, size_t number_of_elements, FILE *a_file);
              
size_t fwrite(const void *ptr, size_t size_of_elements, size_t number_of_elements, FILE *a_file);

Обе эти функции должны использоваться для чтения или записи блоков памяти – обычно массивов или структур.

C – препроцессоры

Препроцессор C не является частью компилятора, но является отдельным этапом в процессе компиляции. Проще говоря, препроцессор C – это всего лишь инструмент подстановки текста, который инструктирует компилятору выполнить необходимую предварительную обработку перед фактической компиляцией. Мы будем называть препроцессор C как CPP.

Все команды препроцессора начинаются с символа хеша (#). Это должен быть первый непустой символ, и для удобства чтения директива препроцессора должна начинаться с первого столбца. В следующем разделе перечислены все важные директивы препроцессора –

Sr.No. Директива и описание
1

#define

Заменяет макрос препроцессора.

2

#включают

Вставляет определенный заголовок из другого файла.

3

#undef

Определяет макрос препроцессора.

4

#ifdef

Возвращает true, если этот макрос определен.

5

#ifndef

Возвращает true, если этот макрос не определен.

6

#если

Проверяет, верно ли условие времени компиляции.

7

#else

Альтернатива для #if.

8

#elif

#else и #if в одном утверждении.

9

#endif

Завершает препроцессор условно.

10

#ошибка

Распечатывает сообщение об ошибке на stderr.

11

#pragma

Выдает специальные команды компилятору, используя стандартизированный метод.

#define

Заменяет макрос препроцессора.

#включают

Вставляет определенный заголовок из другого файла.

#undef

Определяет макрос препроцессора.

#ifdef

Возвращает true, если этот макрос определен.

#ifndef

Возвращает true, если этот макрос не определен.

#если

Проверяет, верно ли условие времени компиляции.

#else

Альтернатива для #if.

#elif

#else и #if в одном утверждении.

#endif

Завершает препроцессор условно.

#ошибка

Распечатывает сообщение об ошибке на stderr.

#pragma

Выдает специальные команды компилятору, используя стандартизированный метод.

Примеры препроцессоров

Проанализируйте следующие примеры, чтобы понять различные директивы.

#define MAX_ARRAY_LENGTH 20

Эта директива указывает CPP заменять экземпляры MAX_ARRAY_LENGTH на 20. Используйте #define для констант для повышения читабельности.

#include <stdio.h>
#include "myheader.h"

Эти директивы сообщают CPP, что нужно получить stdio.h из системных библиотек и добавить текст в текущий исходный файл. Следующая строка говорит CPP, чтобы получить myheader.h из локального каталога и добавить содержимое в текущий исходный файл.

#undef  FILE_SIZE
#define FILE_SIZE 42

Он говорит CPP, чтобы отменить определение существующего FILE_SIZE и определить его как 42.

#ifndef MESSAGE
   #define MESSAGE "You wish!"
#endif

Это говорит CPP, чтобы определить СООБЩЕНИЕ, только если СООБЩЕНИЕ еще не определено.

#ifdef DEBUG
   /* Your debugging statements here */
#endif

Он говорит CPP обрабатывать приложенные операторы, если определен DEBUG. Это полезно, если вы передаете флаг -DDEBUG компилятору gcc во время компиляции. Это определит DEBUG, так что вы можете включать и выключать отладку на лету во время компиляции.

Предопределенные макросы

ANSI C определяет количество макросов. Хотя каждый из них доступен для использования в программировании, предопределенные макросы не должны изменяться напрямую.

Sr.No. Макрос и описание
1

__ДАТА__

Текущая дата в виде символьного литерала в формате “МММ ДД ГГГГ”.

2

__ВРЕМЯ__

Текущее время как символьный литерал в формате «ЧЧ: ММ: СС».

3

__ФАЙЛ__

Это содержит текущее имя файла в виде строкового литерала.

4

__ЛИНИЯ__

Он содержит номер текущей строки в виде десятичной константы.

5

__STDC__

Определяется как 1, когда компилятор соответствует стандарту ANSI.

__ДАТА__

Текущая дата в виде символьного литерала в формате “МММ ДД ГГГГ”.

__ВРЕМЯ__

Текущее время как символьный литерал в формате «ЧЧ: ММ: СС».

__ФАЙЛ__

Это содержит текущее имя файла в виде строкового литерала.

__ЛИНИЯ__

Он содержит номер текущей строки в виде десятичной константы.

__STDC__

Определяется как 1, когда компилятор соответствует стандарту ANSI.

Давайте попробуем следующий пример –

Live Demo

#include <stdio.h>

int main() {

   printf("File :%sn", __FILE__ );
   printf("Date :%sn", __DATE__ );
   printf("Time :%sn", __TIME__ );
   printf("Line :%dn", __LINE__ );
   printf("ANSI :%dn", __STDC__ );

}

Когда приведенный выше код в файле test.c компилируется и выполняется, он дает следующий результат –

File :test.c
Date :Jun 2 2012
Time :03:36:24
Line :8
ANSI :1

Операторы препроцессора

Препроцессор C предлагает следующие операторы для создания макросов:

Оператор продолжения макроса ()

Макрос обычно ограничен одной строкой. Оператор продолжения макроса () используется для продолжения макроса, который слишком длинный для одной строки. Например –

#define  message_for(a, b)  
   printf(#a " and " #b ": We love you!n")

Оператор Stringize (#)

Оператор stringize или number-sign (‘#’), когда используется в определении макроса, преобразует параметр макроса в строковую константу. Этот оператор может использоваться только в макросе с указанным аргументом или списком параметров. Например –

Live Demo

#include <stdio.h>

#define  message_for(a, b)  
   printf(#a " and " #b ": We love you!n")

int main(void) {
   message_for(Carole, Debra);
   return 0;
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

Carole and Debra: We love you!

Оператор вставки токена (##)

Оператор вставки токена (##) в определении макроса объединяет два аргумента. Он позволяет объединить два отдельных токена в определении макроса в один токен. Например –

Live Demo

#include <stdio.h>

#define tokenpaster(n) printf ("token" #n " = %d", token##n)

int main(void) {
   int token34 = 40;
   tokenpaster(34);
   return 0;
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

token34 = 40

Это произошло так, потому что этот пример приводит к следующему фактическому выводу препроцессора –

printf ("token34 = %d", token34);

В этом примере показано объединение токена ## n в token34, и здесь мы использовали как stringize, так и вставку токена .

Определенный () оператор

Определяемый препроцессором оператор используется в константных выражениях, чтобы определить, определен ли идентификатор с помощью #define. Если указанный идентификатор определен, значение равно true (не ноль). Если символ не определен, значение равно false (ноль). Определенный оператор указан следующим образом:

Live Demo

#include <stdio.h>

#if !defined (MESSAGE)
   #define MESSAGE "You wish!"
#endif

int main(void) {
   printf("Here is the message: %sn", MESSAGE);  
   return 0;
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

Here is the message: You wish!

Параметризованные макросы

Одной из мощных функций CPP является возможность имитировать функции с помощью параметризованных макросов. Например, у нас может быть некоторый код для возведения в квадрат числа следующим образом:

int square(int x) {
   return x * x;
}

Мы можем переписать код выше с помощью макроса следующим образом:

#define square(x) ((x) * (x))

Макросы с аргументами должны быть определены с использованием директивы #define, прежде чем их можно будет использовать. Список аргументов заключен в круглые скобки и должен следовать сразу за именем макроса. Пробелы между именем макроса и открытыми скобками не допускаются. Например –

Live Demo

#include <stdio.h>

#define MAX(x,y) ((x) > (y) ? (x) : (y))

int main(void) {
   printf("Max between 20 and 10 is %dn", MAX(10, 20));  
   return 0;
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

Max between 20 and 10 is 20

C – Заголовочные файлы

Заголовочный файл – это файл с расширением .h, который содержит объявления функций C и определения макросов для совместного использования несколькими исходными файлами. Существует два типа заголовочных файлов: файлы, которые пишет программист, и файлы, поставляемые с вашим компилятором.

Вы просите использовать файл заголовка в своей программе, включив его в директиву предварительной обработки C #include , как вы видели включение заголовочного файла stdio.h , который поставляется вместе с вашим компилятором.

Включение файла заголовка равнозначно копированию содержимого файла заголовка, но мы этого не делаем, потому что это будет подвержено ошибкам, и не стоит копировать содержимое файла заголовка в исходные файлы, особенно если мы иметь несколько исходных файлов в программе.

Простая практика в программах на C или C ++ заключается в том, что мы сохраняем все константы, макросы, глобальные переменные в масштабе всей системы и прототипы функций в заголовочных файлах и включаем этот заголовочный файл везде, где это требуется.

Включить синтаксис

И пользовательские, и системные заголовочные файлы включаются с использованием директивы предварительной обработки #include . Он имеет следующие две формы –

#include <file>

Эта форма используется для системных заголовочных файлов. Он ищет файл с именем «file» в стандартном списке системных каталогов. Вы можете добавить каталоги к этому списку с помощью опции -I при компиляции исходного кода.

#include "file"

Эта форма используется для заголовочных файлов вашей собственной программы. Он ищет файл с именем «file» в каталоге, содержащем текущий файл. Вы можете добавить каталоги к этому списку с помощью опции -I при компиляции исходного кода.

Включить операцию

Директива #include работает, заставляя препроцессор C сканировать указанный файл в качестве входных данных, прежде чем продолжить работу с остальной частью текущего исходного файла. Выходные данные препроцессора содержат уже сгенерированные выходные данные, за которыми следуют выходные данные из включенного файла, а затем выходные данные из текста после директивы #include . Например, если у вас есть файл заголовка header.h как показано ниже:

char *test (void);

и основная программа с именем program.c, которая использует заголовочный файл, например:

int x;
#include "header.h"

int main (void) {
   puts (test ());
}

компилятор увидит тот же поток токенов, что и при чтении program.c

int x;
char *test (void);

int main (void) {
   puts (test ());
}

Заголовки только один раз

Если заголовочный файл будет включен дважды, компилятор обработает его содержимое дважды, что приведет к ошибке. Стандартный способ предотвратить это состоит в том, чтобы заключить все реальное содержимое файла в условное выражение, например:

#ifndef HEADER_FILE
#define HEADER_FILE

the entire header file file

#endif

Эта конструкция широко известна как оболочка #ifndef . Когда заголовок включается снова, условное будет ложным, потому что определено HEADER_FILE. Препроцессор пропустит все содержимое файла, и компилятор не увидит его дважды.

Вычисляется Включает

Иногда необходимо выбрать один из нескольких различных заголовочных файлов для включения в вашу программу. Например, они могут указывать параметры конфигурации, которые будут использоваться в разных типах операционных систем. Вы можете сделать это с помощью ряда условий:

#if SYSTEM_1
   # include "system_1.h"
#elif SYSTEM_2
   # include "system_2.h"
#elif SYSTEM_3
   ...
#endif

Но по мере роста он становится утомительным, вместо этого препроцессор предлагает возможность использовать макрос для имени заголовка. Это называется вычисленным включением . Вместо того, чтобы писать имя заголовка в качестве прямого аргумента #include , вы просто помещаете туда имя макроса –

#define SYSTEM_H "system_1.h"
...
#include SYSTEM_H

SYSTEM_H будет расширен, и препроцессор будет искать system_1.h, как если бы изначально был написан #include . SYSTEM_H может быть определен вашим Makefile с опцией -D.

C – Тип литья

Приведение типов – это способ преобразования переменной из одного типа данных в другой тип данных. Например, если вы хотите сохранить значение ‘long’ в простое целое число, вы можете ввести cast ‘long’ в ‘int’. Вы можете явно преобразовать значения из одного типа в другой, используя оператор приведения, следующим образом:

(type_name) expression

Рассмотрим следующий пример, где оператор приведения заставляет деление одной целочисленной переменной на другую как операцию с плавающей запятой:

Live Demo

#include <stdio.h>

main() {

   int sum = 17, count = 5;
   double mean;

   mean = (double) sum / count;
   printf("Value of mean : %fn", mean );
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

Value of mean : 3.400000

Здесь следует отметить, что оператор приведения имеет приоритет над делением, поэтому значение суммы сначала преобразуется в тип double и, наконец, оно делится на число, что приводит к двойному значению.

Преобразования типов могут быть неявными, которые выполняются компилятором автоматически, или они могут быть определены явно с помощью оператора приведения . Хорошей практикой программирования считается использование оператора приведения, когда необходимо преобразование типа.

Целочисленное продвижение

Целочисленное продвижение – это процесс, при котором значения целочисленного типа «меньше», чем int или unsigned int , преобразуются либо в int, либо в unsigned int . Рассмотрим пример добавления символа с целым числом –

Live Demo

#include <stdio.h>

main() {

   int  i = 17;
   char c = 'c'; /* ascii value is 99 */
   int sum;

   sum = i + c;
   printf("Value of sum : %dn", sum );
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

Value of sum : 116

Здесь значение суммы равно 116, потому что компилятор выполняет целочисленное продвижение и преобразовывает значение ‘c’ в ASCII перед выполнением фактической операции сложения.

Обычное Арифметическое Преобразование

Обычные арифметические преобразования неявно выполняются для приведения их значений к общему типу. Компилятор сначала выполняет целочисленное продвижение ; если операнды по-прежнему имеют разные типы, то они преобразуются в тип, который отображается выше в следующей иерархии –

Обычное Арифметическое Преобразование

Обычные арифметические преобразования не выполняются ни для операторов присваивания, ни для логических операторов && и ||. Давайте возьмем следующий пример, чтобы понять концепцию –

Live Demo

#include <stdio.h>

main() {

   int  i = 17;
   char c = 'c'; /* ascii value is 99 */
   float sum;

   sum = i + c;
   printf("Value of sum : %fn", sum );
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

Value of sum : 116.000000

Здесь легко понять, что первый c преобразуется в целое число, но, поскольку конечное значение равно двойному, применяется обычное арифметическое преобразование, и компилятор преобразует i и c в «float» и добавляет их, получая результат «float».

C – Обработка ошибок

Таким образом, программирование на C не обеспечивает прямой поддержки обработки ошибок, но, будучи языком системного программирования, оно предоставляет вам доступ на более низком уровне в форме возвращаемых значений. Большинство вызовов функций C или даже Unix возвращают -1 или NULL в случае любой ошибки и устанавливают код ошибки errno . Он устанавливается как глобальная переменная и указывает на ошибку, возникшую во время любого вызова функции. Вы можете найти различные коды ошибок, определенные в заголовочном файле <error.h>.

Таким образом, программист C может проверить возвращаемые значения и может предпринять соответствующие действия в зависимости от возвращаемого значения. Рекомендуется устанавливать значение errno равным 0 во время инициализации программы. Значение 0 указывает на то, что в программе нет ошибок.

ошибка, perror (). и strerror ()

Язык программирования C предоставляет функции perror () и strerror (), которые можно использовать для отображения текстового сообщения, связанного с errno .

  • Функция perror () отображает строку, которую вы передаете ей, затем двоеточие, пробел, а затем текстовое представление текущего значения errno.

  • Функция strerror () , которая возвращает указатель на текстовое представление текущего значения errno.

Функция perror () отображает строку, которую вы передаете ей, затем двоеточие, пробел, а затем текстовое представление текущего значения errno.

Функция strerror () , которая возвращает указатель на текстовое представление текущего значения errno.

Давайте попробуем смоделировать состояние ошибки и попытаться открыть файл, который не существует. Здесь я использую обе функции, чтобы показать использование, но вы можете использовать один или несколько способов печати ваших ошибок. Вторым важным моментом, который следует отметить, является то, что вы должны использовать поток файлов stderr для вывода всех ошибок.

#include <stdio.h>
#include <errno.h>
#include <string.h>

extern int errno ;

int main () {

   FILE * pf;
   int errnum;
   pf = fopen ("unexist.txt", "rb");
	
   if (pf == NULL) {
   
      errnum = errno;
      fprintf(stderr, "Value of errno: %dn", errno);
      perror("Error printed by perror");
      fprintf(stderr, "Error opening file: %sn", strerror( errnum ));
   } else {
   
      fclose (pf);
   }
   
   return 0;
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

Value of errno: 2
Error printed by perror: No such file or directory
Error opening file: No such file or directory

Разделить на ноль ошибок

Общей проблемой является то, что во время деления любого числа программисты не проверяют, равен ли делитель нулю, и, наконец, он создает ошибку времени выполнения.

Код ниже исправляет это, проверяя, равен ли делитель нулю перед делением –

Live Demo

#include <stdio.h>
#include <stdlib.h>

main() {

   int dividend = 20;
   int divisor = 0;
   int quotient;
 
   if( divisor == 0){
      fprintf(stderr, "Division by zero! Exiting...n");
      exit(-1);
   }
   
   quotient = dividend / divisor;
   fprintf(stderr, "Value of quotient : %dn", quotient );

   exit(0);
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

Division by zero! Exiting...

Состояние выхода из программы

Обычной практикой является выход со значением EXIT_SUCCESS в случае выхода программы после успешной операции. Здесь EXIT_SUCCESS является макросом, и он определен как 0.

Если в вашей программе возникла ошибка, и вы выходите из нее, вам следует выйти со статусом EXIT_FAILURE, который определен как -1. Итак, давайте напишем выше программу следующим образом –

Live Demo

#include <stdio.h>
#include <stdlib.h>

main() {

   int dividend = 20;
   int divisor = 5;
   int quotient;
 
   if( divisor == 0) {
      fprintf(stderr, "Division by zero! Exiting...n");
      exit(EXIT_FAILURE);
   }
	
   quotient = dividend / divisor;
   fprintf(stderr, "Value of quotient : %dn", quotient );

   exit(EXIT_SUCCESS);
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

Value of quotient : 4

C – рекурсия

Рекурсия – это процесс повторения предметов самоподобным способом. В языках программирования, если программа позволяет вам вызывать функцию внутри одной и той же функции, то это называется рекурсивным вызовом функции.

void recursion() {
   recursion(); /* function calls itself */
}

int main() {
   recursion();
}

Язык программирования C поддерживает рекурсию, то есть функцию, которая вызывает сама себя. Но при использовании рекурсии программистам нужно быть осторожными, чтобы определить условие выхода из функции, иначе оно войдет в бесконечный цикл.

Рекурсивные функции очень полезны для решения многих математических задач, таких как вычисление факториала числа, генерация рядов Фибоначчи и т. Д.

Номер Факториал

В следующем примере вычисляется факториал данного числа с использованием рекурсивной функции –

Live Demo

#include <stdio.h>

unsigned long long int factorial(unsigned int i) {

   if(i <= 1) {
      return 1;
   }
   return i * factorial(i - 1);
}

int  main() {
   int i = 12;
   printf("Factorial of %d is %dn", i, factorial(i));
   return 0;
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

Factorial of 12 is 479001600

Серия Фибоначчи

В следующем примере генерируется ряд Фибоначчи для заданного числа с использованием рекурсивной функции –

Live Demo

#include <stdio.h>

int fibonacci(int i) {

   if(i == 0) {
      return 0;
   }
	
   if(i == 1) {
      return 1;
   }
   return fibonacci(i-1) + fibonacci(i-2);
}

int  main() {

   int i;
	
   for (i = 0; i < 10; i++) {
      printf("%dtn", fibonacci(i));
   }
	
   return 0;
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат –

0	
1	
1	
2	
3	
5	
8	
13	
21	
34

C – Переменные аргументы

Иногда вы можете столкнуться с ситуацией, когда вы хотите иметь функцию, которая может принимать переменное количество аргументов, то есть параметров, вместо предопределенного количества параметров. Язык программирования C предоставляет решение для этой ситуации, и вы можете определить функцию, которая может принимать переменное количество параметров в зависимости от ваших требований. В следующем примере показано определение такой функции.

int func(int, ... ) {
   .
   .
   .
}

int main() {
   func(1, 2, 3);
   func(1, 2, 3, 4);
}

Следует отметить, что функция func () имеет свой последний аргумент в виде эллипсов, то есть три точки ( ) и одна перед эллипсами всегда представляет собой целое число, представляющее общее число переданных аргументов переменной. Чтобы использовать такую ​​функциональность, вам нужно использовать файл заголовка stdarg.h, который предоставляет функции и макросы для реализации функциональности переменных аргументов и выполнения указанных шагов –

  • Определите функцию с последним параметром в виде эллипсов, а перед параметром эллипсы всегда будет int, который будет представлять число аргументов.

  • Создайте переменную типа va_list в определении функции. Этот тип определен в заголовочном файле stdarg.h.

  • Используйте параметр int и макрос va_start, чтобы инициализировать переменную va_list в список аргументов. Макрос va_start определен в заголовочном файле stdarg.h.

  • Используйте макрос va_arg и переменную va_list для доступа к каждому элементу в списке аргументов.

  • Используйте макрос va_end, чтобы очистить память, назначенную переменной va_list .

Определите функцию с последним параметром в виде эллипсов, а перед параметром эллипсы всегда будет int, который будет представлять число аргументов.

Создайте переменную типа va_list в определении функции. Этот тип определен в заголовочном файле stdarg.h.

Используйте параметр int и макрос va_start, чтобы инициализировать переменную va_list в список аргументов. Макрос va_start определен в заголовочном файле stdarg.h.

Используйте макрос va_arg и переменную va_list для доступа к каждому элементу в списке аргументов.

Используйте макрос va_end, чтобы очистить память, назначенную переменной va_list .

Теперь давайте выполним описанные выше шаги и запишем простую функцию, которая может принимать переменное количество параметров и возвращать их среднее значение –

Live Demo

#include <stdio.h>
#include <stdarg.h>

double average(int num,...) {

   va_list valist;
   double sum = 0.0;
   int i;

   /* initialize valist for num number of arguments */
   va_start(valist, num);

   /* access all the arguments assigned to valist */
   for (i = 0; i < num; i++) {
      sum += va_arg(valist, int);
   }
	
   /* clean memory reserved for valist */
   va_end(valist);

   return sum/num;
}

int main() {
   printf("Average of 2, 3, 4, 5 = %fn", average(4, 2,3,4,5));
   printf("Average of 5, 10, 15 = %fn", average(3, 5,10,15));
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат. Следует отметить, что функция Average () вызывается дважды, и каждый раз, когда первый аргумент представляет общее количество передаваемых переменных переменных. Только эллипсы будут использоваться для передачи переменного количества аргументов.

Average of 2, 3, 4, 5 = 3.500000
Average of 5, 10, 15 = 10.000000

C – Управление памятью

В этой главе описывается динамическое управление памятью на языке C. Язык программирования C предоставляет несколько функций для распределения памяти и управления ею. Эти функции можно найти в заголовочном файле <stdlib.h> .

Sr.No. Описание функции
1

void * calloc (int num, int size);

Эта функция выделяет массив из num элементов, каждый из которых будет иметь размер в байтах.

2

void free (void * address);

Эта функция освобождает блок памяти, указанный в адресе.

3

void * malloc (int num);

Эта функция выделяет массив из num байтов и оставляет их неинициализированными.

4

void * realloc (void * address, int newsize);

Эта функция перераспределяет память, расширяя ее до новостей .

void * calloc (int num, int size);

Эта функция выделяет массив из num элементов, каждый из которых будет иметь размер в байтах.

void free (void * address);

Эта функция освобождает блок памяти, указанный в адресе.

void * malloc (int num);

Эта функция выделяет массив из num байтов и оставляет их неинициализированными.

void * realloc (void * address, int newsize);

Эта функция перераспределяет память, расширяя ее до новостей .

Выделение памяти динамически

При программировании, если вы знаете размер массива, это легко, и вы можете определить его как массив. Например, чтобы сохранить имя любого человека, оно может содержать до 100 символов, поэтому вы можете определить что-то следующим образом:

char name[100];

Но теперь давайте рассмотрим ситуацию, когда у вас нет представления о длине текста, который нужно сохранить, например, вы хотите сохранить подробное описание по теме. Здесь нам нужно определить указатель на символ, не определяя, сколько памяти требуется, и позже, в зависимости от потребности, мы можем выделить память, как показано в следующем примере –

Live Demo

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {

   char name[100];
   char *description;

   strcpy(name, "Zara Ali");

   /* allocate memory dynamically */
   description = malloc( 200 * sizeof(char) );
	
   if( description == NULL ) {
      fprintf(stderr, "Error - unable to allocate required memoryn");
   } else {
      strcpy( description, "Zara ali a DPS student in class 10th");
   }
   
   printf("Name = %sn", name );
   printf("Description: %sn", description );
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат.

Name = Zara Ali
Description: Zara ali a DPS student in class 10th

Эту же программу можно написать с помощью calloc (); единственное, вам нужно заменить malloc на calloc следующим образом:

calloc(200, sizeof(char));

Таким образом, вы имеете полный контроль и можете передавать любое значение размера при выделении памяти, в отличие от массивов, где после определения размера вы не можете его изменить.

Изменение размера и освобождение памяти

Когда ваша программа выходит, операционная система автоматически освобождает всю память, выделенную вашей программой, но в качестве хорошей практики, когда вам больше не нужна память, вы должны освободить эту память, вызвав функцию free () .

Кроме того, вы можете увеличить или уменьшить размер выделенного блока памяти, вызвав функцию realloc () . Давайте еще раз проверим вышеуказанную программу и воспользуемся функциями realloc () и free () –

Live Demo

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {

   char name[100];
   char *description;

   strcpy(name, "Zara Ali");

   /* allocate memory dynamically */
   description = malloc( 30 * sizeof(char) );
	
   if( description == NULL ) {
      fprintf(stderr, "Error - unable to allocate required memoryn");
   } else {
      strcpy( description, "Zara ali a DPS student.");
   }
	
   /* suppose you want to store bigger description */
   description = realloc( description, 100 * sizeof(char) );
	
   if( description == NULL ) {
      fprintf(stderr, "Error - unable to allocate required memoryn");
   } else {
      strcat( description, "She is in class 10th");
   }
   
   printf("Name = %sn", name );
   printf("Description: %sn", description );

   /* release memory using free() function */
   free(description);
}

Когда приведенный выше код компилируется и выполняется, он дает следующий результат.

Name = Zara Ali
Description: Zara ali a DPS student.She is in class 10th

Вы можете попробовать приведенный выше пример без перераспределения дополнительной памяти, а функция strcat () выдаст ошибку из-за нехватки доступной памяти в описании.

C – Аргументы командной строки

Можно передать некоторые значения из командной строки вашим программам на Си, когда они выполняются. Эти значения называются аргументами командной строки, и во многих случаях они важны для вашей программы, особенно когда вы хотите управлять своей программой извне, а не жестко кодировать эти значения внутри кода.

Аргументы командной строки обрабатываются с помощью аргументов функции main (), где argc ссылается на количество переданных аргументов, а argv [] – массив указателей, который указывает на каждый аргумент, переданный программе. Ниже приведен простой пример, который проверяет, есть ли какой-либо аргумент из командной строки, и предпринимает соответствующие действия:

#include <stdio.h>

int main( int argc, char *argv[] ) {

   if( argc == 2 ) {
      printf("The argument supplied is %sn", argv[1]);
   } else if( argc > 2 ) {
      printf("Too many arguments supplied.n");
   } else {
      printf("One argument expected.n");
   }
}

Когда приведенный выше код компилируется и выполняется с одним аргументом, он дает следующий результат.

$./a.out testing
The argument supplied is testing

Когда приведенный выше код компилируется и выполняется с двумя аргументами, он дает следующий результат.

$./a.out testing1 testing2
Too many arguments supplied.

Когда приведенный выше код компилируется и выполняется без передачи аргумента, он дает следующий результат.

$./a.out
One argument expected

Следует отметить, что argv [0] содержит имя самой программы, а argv [1] – указатель на первый предоставленный аргумент командной строки, а * argv [n] – последний аргумент. Если аргументы не предоставлены, argc будет один, а если вы передадите один аргумент, argc будет установлен в 2.

Вы передаете все аргументы командной строки, разделенные пробелом, но если сам аргумент имеет пробел, вы можете передать такие аргументы, заключив их в двойные кавычки “” или одинарные кавычки “. Давайте еще раз напишем приведенный выше пример, где мы напечатаем имя программы, а также передадим аргумент командной строки, заключив в двойные кавычки –

#include <stdio.h>

int main( int argc, char *argv[] ) {

   printf("Program name %sn", argv[0]);
 
   if( argc == 2 ) {
      printf("The argument supplied is %sn", argv[1]);
   } else if( argc > 2 ) {
      printf("Too many arguments supplied.n");
   } else {
      printf("One argument expected.n");
   }
}

Когда приведенный выше код компилируется и выполняется с одним аргументом, разделенным пробелом, но внутри двойных кавычек, он дает следующий результат.


Понравилась статья? Поделить с друзьями:

А вот и еще интересные новости по теме:

  • Jura impressa e75 инструкция на русском
  • Триосепт эндо инструкция по применению в медицине
  • Панцеф 400 мг инструкция по применению цена детям
  • Оргавит костная мука инструкция по применению
  • Инструкция по правилам безопасного поведения на объектах железнодорожного транспорта

  • 0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии